diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000..fecaada --- /dev/null +++ b/go/.gitignore @@ -0,0 +1,6 @@ +# Prebuilt cstx-ffi static libraries are delivered by the cstx release +# pipeline (synced with git add -f into libcstx), never built in-tree. +lib/**/*.a +lib/**/*.lib +lib/**/*.so +lib/**/*.dylib diff --git a/go/cas.go b/go/cas.go deleted file mode 100755 index 2bfe0fc..0000000 --- a/go/cas.go +++ /dev/null @@ -1,167 +0,0 @@ -package cstx - -/* -#include "cstx_ffi.h" -#include -*/ -import "C" -import ( - "encoding/json" - "fmt" - "unsafe" -) - -type CASStore struct { - ptr *C.struct_CstxCasStore -} - -type TreeDiff struct { - Added map[string][]string `json:"added"` - Removed map[string][]string `json:"removed"` - Modified map[string][]string `json:"modified"` -} - -func NewCASStore() *CASStore { - return &CASStore{ptr: C.cstx_cas_store_new()} -} - -func (s *CASStore) Close() { - if s.ptr != nil { - C.cstx_cas_store_free(s.ptr) - s.ptr = nil - } -} - -func CanonicalHash(data []byte) string { - cJSON := C.CString(string(data)) - defer C.free(unsafe.Pointer(cJSON)) - out := C.cstx_cas_canonical_hash(cJSON) - if out == nil { - return "" - } - defer C.cstx_free_string(out) - return C.GoString(out) -} - -func (s *CASStore) LoadRoot(hash string, data []byte) error { - if s == nil || s.ptr == nil { - return fmt.Errorf("cstx: CAS store is closed") - } - cHash := C.CString(hash) - cJSON := C.CString(string(data)) - defer C.free(unsafe.Pointer(cHash)) - defer C.free(unsafe.Pointer(cJSON)) - if C.cstx_cas_store_load_root(s.ptr, cHash, cJSON) != 0 { - return fmt.Errorf("cstx: load root failed") - } - return nil -} - -func (s *CASStore) LoadMap(hash string, data []byte) error { - if s == nil || s.ptr == nil { - return fmt.Errorf("cstx: CAS store is closed") - } - cHash := C.CString(hash) - cJSON := C.CString(string(data)) - defer C.free(unsafe.Pointer(cHash)) - defer C.free(unsafe.Pointer(cJSON)) - if C.cstx_cas_store_load_map(s.ptr, cHash, cJSON) != 0 { - return fmt.Errorf("cstx: load map failed") - } - return nil -} - -func (s *CASStore) Export(rootHash string) ([]byte, error) { - if s == nil || s.ptr == nil { - return nil, fmt.Errorf("cstx: CAS store is closed") - } - cRoot := C.CString(rootHash) - defer C.free(unsafe.Pointer(cRoot)) - out := C.cstx_cas_store_export(s.ptr, cRoot) - if out == nil { - return nil, fmt.Errorf("cstx: export failed") - } - defer C.cstx_free_string(out) - return []byte(C.GoString(out)), nil -} - -func (s *CASStore) TreeBuild(entriesJSON []byte) (string, error) { - if s == nil || s.ptr == nil { - return "", fmt.Errorf("cstx: CAS store is closed") - } - cJSON := C.CString(string(entriesJSON)) - defer C.free(unsafe.Pointer(cJSON)) - out := C.cstx_cas_tree_build(s.ptr, cJSON) - if out == nil { - return "", fmt.Errorf("cstx: tree build failed") - } - defer C.cstx_free_string(out) - return C.GoString(out), nil -} - -func (s *CASStore) TreeUpdate(rootHash string, changesJSON []byte) (string, error) { - if s == nil || s.ptr == nil { - return "", fmt.Errorf("cstx: CAS store is closed") - } - cRoot := C.CString(rootHash) - cJSON := C.CString(string(changesJSON)) - defer C.free(unsafe.Pointer(cRoot)) - defer C.free(unsafe.Pointer(cJSON)) - out := C.cstx_cas_tree_update(s.ptr, cRoot, cJSON) - if out == nil { - return "", fmt.Errorf("cstx: tree update failed") - } - defer C.cstx_free_string(out) - return C.GoString(out), nil -} - -func (s *CASStore) TreeDiff(baseHash, headHash string) (*TreeDiff, error) { - if s == nil || s.ptr == nil { - return nil, fmt.Errorf("cstx: CAS store is closed") - } - cBase := C.CString(baseHash) - cHead := C.CString(headHash) - defer C.free(unsafe.Pointer(cBase)) - defer C.free(unsafe.Pointer(cHead)) - out := C.cstx_cas_tree_diff(s.ptr, cBase, cHead) - if out == nil { - return nil, fmt.Errorf("cstx: tree diff failed") - } - defer C.cstx_free_string(out) - - var diff TreeDiff - if err := json.Unmarshal([]byte(C.GoString(out)), &diff); err != nil { - return nil, err - } - return &diff, nil -} - -func (s *CASStore) TreeFind(rootHash, elementID string) (string, bool) { - if s == nil || s.ptr == nil { - return "", false - } - cRoot := C.CString(rootHash) - cID := C.CString(elementID) - defer C.free(unsafe.Pointer(cRoot)) - defer C.free(unsafe.Pointer(cID)) - out := C.cstx_cas_tree_find(s.ptr, cRoot, cID) - if out == nil { - return "", false - } - defer C.cstx_free_string(out) - return C.GoString(out), true -} - -func (s *CASStore) TreeEntries(rootHash string) ([]byte, error) { - if s == nil || s.ptr == nil { - return nil, fmt.Errorf("cstx: CAS store is closed") - } - cRoot := C.CString(rootHash) - defer C.free(unsafe.Pointer(cRoot)) - out := C.cstx_cas_tree_entries(s.ptr, cRoot) - if out == nil { - return nil, fmt.Errorf("cstx: tree entries failed") - } - defer C.cstx_free_string(out) - return []byte(C.GoString(out)), nil -} diff --git a/go/cgo_darwin_amd64.go b/go/cgo_darwin_amd64.go deleted file mode 100644 index e575430..0000000 --- a/go/cgo_darwin_amd64.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build darwin && amd64 - -package cstx - -// #cgo LDFLAGS: -L${SRCDIR}/lib/darwin_amd64 -lcstx_ffi -lm -framework Security -framework CoreFoundation -// #cgo CFLAGS: -I${SRCDIR} -import "C" diff --git a/go/cgo_darwin_arm64.go b/go/cgo_darwin_arm64.go deleted file mode 100644 index a6d11d2..0000000 --- a/go/cgo_darwin_arm64.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build darwin && arm64 - -package cstx - -// #cgo LDFLAGS: -L${SRCDIR}/lib/darwin_arm64 -lcstx_ffi -lm -framework Security -framework CoreFoundation -// #cgo CFLAGS: -I${SRCDIR} -import "C" diff --git a/go/cgo_linux_amd64.go b/go/cgo_linux_amd64.go deleted file mode 100644 index 2e379c0..0000000 --- a/go/cgo_linux_amd64.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build linux && amd64 - -package cstx - -// #cgo LDFLAGS: -L${SRCDIR}/lib/linux_amd64 -lcstx_ffi -lm -ldl -lpthread -// #cgo CFLAGS: -I${SRCDIR} -import "C" diff --git a/go/cgo_linux_arm64.go b/go/cgo_linux_arm64.go deleted file mode 100644 index 528e053..0000000 --- a/go/cgo_linux_arm64.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build linux && arm64 - -package cstx - -// #cgo LDFLAGS: -L${SRCDIR}/lib/linux_arm64 -lcstx_ffi -lm -ldl -lpthread -// #cgo CFLAGS: -I${SRCDIR} -import "C" diff --git a/go/cgo_windows_amd64.go b/go/cgo_windows_amd64.go deleted file mode 100644 index 6a9279a..0000000 --- a/go/cgo_windows_amd64.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build windows && amd64 - -package cstx - -// #cgo LDFLAGS: -L${SRCDIR}/lib/windows_amd64 -lcstx_ffi -lm -lpthread -lws2_32 -luserenv -lbcrypt -lntdll -// #cgo CFLAGS: -I${SRCDIR} -import "C" diff --git a/go/contract_test.go b/go/contract_test.go new file mode 100644 index 0000000..ff49119 --- /dev/null +++ b/go/contract_test.go @@ -0,0 +1,42 @@ +package cstx + +import ( + "reflect" + "sort" + "strings" + "testing" +) + +// The transport contract is owned by cstx.core.elements (Node/Edge pydantic +// models) and mirrored by codegen/generate_transport_ts.py. These tests pin +// the Go structs to the same field sets so the three languages cannot drift. +func jsonFieldNames(t *testing.T, value any) []string { + t.Helper() + typ := reflect.TypeOf(value) + var names []string + for i := 0; i < typ.NumField(); i++ { + tag := typ.Field(i).Tag.Get("json") + if tag == "" || tag == "-" { + continue + } + names = append(names, strings.Split(tag, ",")[0]) + } + sort.Strings(names) + return names +} + +func TestNodeMatchesTransportContract(t *testing.T) { + got := jsonFieldNames(t, Node{}) + want := []string{"extras", "id", "model", "sources", "type", "value"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("node fields drifted from transport contract: got %v want %v", got, want) + } +} + +func TestEdgeMatchesTransportContract(t *testing.T) { + got := jsonFieldNames(t, Edge{}) + want := []string{"attrs", "id", "relation_type", "source_id", "sources", "target_id"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("edge fields drifted from transport contract: got %v want %v", got, want) + } +} diff --git a/go/cstx.go b/go/cstx.go new file mode 100644 index 0000000..da7832f --- /dev/null +++ b/go/cstx.go @@ -0,0 +1,97 @@ +package cstx + +import ( + "context" + "errors" + "sync" +) + +// Config configures an in-memory CSTX runtime. +type Config struct { + // ProjectID namespaces the runtime; defaults to "default". + ProjectID string + // CursorPageSize bounds incremental cursor materialization; defaults to + // DefaultCursorPageSize. + CursorPageSize int +} + +func (c Config) normalize() Config { + if c.ProjectID == "" { + c.ProjectID = "default" + } + if c.CursorPageSize <= 0 { + c.CursorPageSize = DefaultCursorPageSize + } + return c +} + +// CSTX is the single owner of shared schema, graph, and repository +// state. Create it with Open and release it with Close. +type CSTX struct { + eng engine + projectID string + + // Schemas, Graph, and Repo are lightweight namespaces sharing + // this runtime's state. + Schemas *Schemas + Graph *Graph + Repo *Repository + + mu sync.Mutex + closed bool +} + +// Open creates an in-memory runtime. Without the cstx_native build tag it +// returns ErrNativeUnavailable. +func Open(ctx context.Context, config Config) (*CSTX, error) { + if err := contextError(ctx); err != nil { + return nil, err + } + config = config.normalize() + eng, err := newEngine(config) + if err != nil { + return nil, err + } + rt := &CSTX{eng: eng, projectID: config.ProjectID} + rt.Schemas = &Schemas{eng: eng} + rt.Graph = &Graph{eng: eng} + rt.Repo = &Repository{eng: eng} + return rt, nil +} + +func contextError(ctx context.Context) error { + if ctx == nil { + return errors.New("cstx: nil context") + } + return ctx.Err() +} + +// ProjectID returns the immutable project namespace supplied at Open. +func (c *CSTX) ProjectID() string { return c.projectID } + +// Close releases shared state and invalidates retained services and cursors. +// Repeated calls are safe. +func (c *CSTX) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed { + return nil + } + c.closed = true + return c.eng.close() +} + +// Closed reports whether Close has been called. +func (c *CSTX) Closed() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.closed +} + +// LastChange returns the IDs changed by the most recent committed mutation. +func (c *CSTX) LastChange(ctx context.Context) (ChangeSet, error) { + if err := contextError(ctx); err != nil { + return ChangeSet{}, err + } + return c.eng.lastChange(ctx) +} diff --git a/go/cstx_ffi.h b/go/cstx_ffi.h index 60f6e64..bf3225d 100644 --- a/go/cstx_ffi.h +++ b/go/cstx_ffi.h @@ -8,282 +8,178 @@ #include #include -typedef struct CstxCasStore CstxCasStore; - -typedef struct CstxGraph CstxGraph; - -struct CstxGraph *cstx_graph_new(void); - -void cstx_graph_free(struct CstxGraph *g); - -int cstx_graph_load_plugin(struct CstxGraph *g, const char *name); - -int cstx_graph_load_all_plugins(struct CstxGraph *g); - -char *cstx_available_plugins(void); - -int cstx_graph_register_schema(struct CstxGraph *g, - const char *node_type, - const char *json_schema_str, - const char *value_field); - -int cstx_graph_add_join_rule(struct CstxGraph *g, - const char *left_type, - const char *right_type, - const char *relation, - const char *left_key, - const char *right_key, - int predicted, - const char *left_target_id, - const char *right_source_id); - -int cstx_graph_add_node(struct CstxGraph *g, - const char *node_type, - const char *cstx_id, - const char *payload_json, - const char *sources_json, - uint64_t cstx_flags); - -int cstx_graph_add_edge(struct CstxGraph *g, - const char *source_id, - const char *target_id, - const char *relation, - const char *data_source); - -int cstx_graph_add_nodes_batch(struct CstxGraph *g, - const char *nodes_json, - char **out, - uintptr_t *out_len); - -int cstx_graph_add_edges_batch(struct CstxGraph *g, const char *edges_json); - -int cstx_graph_link(struct CstxGraph *g, - const char *source, - const uint8_t *data, - uintptr_t data_len, - char **out, - uintptr_t *out_len); - -int cstx_graph_ingest_native(struct CstxGraph *g, - const char *plugin_name, - const char *artifact, - const uint8_t *data, - uintptr_t data_len, - char **out, - uintptr_t *out_len); - -int cstx_graph_has_native_artifact(struct CstxGraph *g, const char *artifact); - -int cstx_graph_ingest_jsonl(struct CstxGraph *g, - const char *node_type, - const uint8_t *data, - uintptr_t data_len, - const char *id_expr, - const char *data_source, - char **out, - uintptr_t *out_len); - -int cstx_graph_link_nodes(struct CstxGraph *g, - const char *data_source, - char **out, - uintptr_t *out_len); - -char *cstx_graph_node(struct CstxGraph *g, const char *node_id); - -char *cstx_graph_nodes(struct CstxGraph *g, const char *type_filter); - -char *cstx_graph_edges(struct CstxGraph *g, const char *relation); - -char *cstx_graph_node_types(struct CstxGraph *g); - -char *cstx_graph_neighbors(struct CstxGraph *g, const char *node_id); - -char *cstx_graph_to_json(struct CstxGraph *g); - -char *cstx_graph_node_payload(struct CstxGraph *g, const char *node_id); - -int cstx_graph_contains_node(struct CstxGraph *g, const char *node_id); - -uintptr_t cstx_graph_node_count(struct CstxGraph *g); - -uintptr_t cstx_graph_edge_count(struct CstxGraph *g); - -char *cstx_graph_node_ids(struct CstxGraph *g); - -char *cstx_graph_stats(struct CstxGraph *g); - -char *cstx_graph_all_nodes_json(struct CstxGraph *g); - -char *cstx_graph_neighbor_ids(struct CstxGraph *g, const char *node_id, const char *direction); - -char *cstx_graph_bfs(struct CstxGraph *g, const char *seed_id, uint32_t depth, int reverse); - -char *cstx_graph_shortest_paths(struct CstxGraph *g, - const char *start_id, - const char *end_id, - uint32_t max_depth); - -uintptr_t cstx_graph_degree(struct CstxGraph *g, const char *node_id, const char *direction); - -int cstx_graph_subgraph_node_ids(struct CstxGraph *g, - const char *seed_ids_json, - uint32_t depth, - char **out, - uintptr_t *out_len); - -int cstx_graph_query_dsl(struct CstxGraph *g, - const char *expression, - intptr_t limit, - uintptr_t offset, - char **out, - uintptr_t *out_len); - -char *cstx_graph_query_node_ids(struct CstxGraph *g, - const char *expression, - intptr_t limit, - uintptr_t offset); - -int cstx_graph_is_path_expression(struct CstxGraph *g, const char *expression); - -char *cstx_graph_edges_filtered(struct CstxGraph *g, - const char *source_id, - const char *target_id, - const char *relation); - -char *cstx_graph_export_snapshot_json(struct CstxGraph *g); - -int cstx_transform(const char *source_type, - const uint8_t *data, - uintptr_t data_len, - char **out, - uintptr_t *out_len); - -void cstx_free_string(char *s); - -const char *cstx_version(void); - -char *cstx_supported_artifacts(void); - -uint64_t cstx_flags_all_mask(void); - -uint64_t cstx_flags_default_exclude_mask(void); - -int cstx_graph_update_node_flags(struct CstxGraph *g, - const char *node_id, - uint64_t add, - uint64_t remove, - int64_t set_to); - -struct CstxCasStore *cstx_cas_store_new(void); - -void cstx_cas_store_free(struct CstxCasStore *s); - -/** - * Load a root tree object into the store. - * kind: "root" | "map" — determines how to parse the JSON. - * For "root": JSON is {"node_types":{"type":"hash",...},"edge_types":{...}} - * For "map": JSON is {"key":"hash",...} - */ -int cstx_cas_store_load_root(struct CstxCasStore *s, const char *hash, const char *json_str); - -/** - * Load a map (type-tree or bucket) object into the store. - */ -int cstx_cas_store_load_map(struct CstxCasStore *s, const char *hash, const char *json_str); - -/** - * Export all tree objects from the store as JSON. - * Returns: {"hash": {"key": "value", ...}, ...} - */ -char *cstx_cas_store_export(struct CstxCasStore *s, const char *root_hash); - -/** - * SHA-256 of canonical JSON. Caller frees result. - */ -char *cstx_cas_canonical_hash(const char *json_str); - -/** - * Build a Merkle tree from entries JSON. - * entries_json: {"type": {"id": "hash", ...}, ...} - * Returns root hash. Caller frees. - */ -char *cstx_cas_tree_build(struct CstxCasStore *s, const char *entries_json); - -/** - * Update a Merkle tree incrementally. - * changes_json: {"type": {"id": "new_hash_or_null", ...}, ...} - * null values = delete. Returns new root hash. Caller frees. - */ -char *cstx_cas_tree_update(struct CstxCasStore *s, const char *root_hash, const char *changes_json); - -/** - * Diff two Merkle trees. Returns JSON: {"added":{},"removed":{},"modified":{}} - * Caller frees. - */ -char *cstx_cas_tree_diff(struct CstxCasStore *s, const char *base_hash, const char *head_hash); - -/** - * Find an element in the tree by ID (searches all types). - * Returns content hash or NULL if not found. Caller frees. - */ -char *cstx_cas_tree_find(struct CstxCasStore *s, const char *root_hash, const char *element_id); - -/** - * Materialize all entries from a tree. - * Returns JSON: {"type": {"id": "hash", ...}, ...}. Caller frees. - */ -char *cstx_cas_tree_entries(struct CstxCasStore *s, const char *root_hash); - -int cstx_graph_semantic_texts(struct CstxGraph *g, - const char *node_types_json, - char **out, - uintptr_t *out_len); - -void cstx_graph_init_rag(struct CstxGraph *g, uintptr_t dims); - -/** - * entries_json: [[arena_idx, "node_type", [f32, ...]], ...] - */ -int cstx_graph_set_vectors(struct CstxGraph *g, - const char *entries_json, - char **out, - uintptr_t *out_len); - -int cstx_graph_build_indices(struct CstxGraph *g, char **out, uintptr_t *out_len); - -/** - * Returns JSON: [[arena_idx, score, "cstx_id"], ...] - */ -int cstx_graph_rag_search(struct CstxGraph *g, - const char *query_text, - const char *query_vector_json, - const char *strategy, - const char *node_types_json, - uintptr_t top_k, - char **out, - uintptr_t *out_len); - -/** - * Returns JSON: {"count": N, "modularity": f64} - */ -int cstx_graph_detect_communities(struct CstxGraph *g, - double resolution, - uintptr_t min_community_size, - char **out, - uintptr_t *out_len); - -int cstx_graph_set_community_summary(struct CstxGraph *g, - uint32_t community_id, - const char *summary, - char **out, - uintptr_t *out_len); +enum CstxStatusCode +#if __STDC_VERSION__ >= 202311L + : int32_t +#endif // __STDC_VERSION__ >= 202311L + { + Ok = 0, + InvalidArgument = 1, + NotFound = 2, + Conflict = 3, + NotInitialized = 4, + Unsupported = 5, + Internal = 6, + Validation = 7, + Parse = 8, + Io = 9, + CorruptData = 10, + CursorInvalidated = 11, +}; +#if __STDC_VERSION__ >= 202311L +typedef enum CstxStatusCode CstxStatusCode; +#else +typedef int32_t CstxStatusCode; +#endif // __STDC_VERSION__ >= 202311L + +typedef struct CstxEdgeIterator CstxEdgeIterator; + +typedef struct CstxHandle CstxHandle; + +typedef struct CstxNodeIterator CstxNodeIterator; + +typedef struct CstxBuffer { + uint8_t *data; + uintptr_t len; +} CstxBuffer; + +typedef struct CstxSlice { + const uint8_t *data; + uintptr_t len; +} CstxSlice; /** - * Returns JSON array of arena indices: [u32, ...] + * Release a Rust-owned output or error buffer. */ -int cstx_graph_community_members(struct CstxGraph *g, - uint32_t community_id, - char **out, - uintptr_t *out_len); +void cstx_buffer_free(struct CstxBuffer *buffer); + +CstxStatusCode cstx_open(struct CstxSlice config_json, + struct CstxHandle **output, + struct CstxBuffer *error); + +void cstx_close(struct CstxHandle *handle); + +void cstx_free(struct CstxHandle *handle); + +CstxStatusCode cstx_last_change_json(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_register(struct CstxHandle *handle, + struct CstxSlice node_type, + struct CstxSlice schema_json, + struct CstxSlice value_field, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_contains(struct CstxHandle *handle, + struct CstxSlice node_type, + uint8_t *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_get_json(struct CstxHandle *handle, + struct CstxSlice node_type, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_load_plugin(struct CstxHandle *handle, + struct CstxSlice name, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_load_all_plugins(struct CstxHandle *handle, struct CstxBuffer *error); + +CstxStatusCode cstx_schema_available_plugins_json(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_ingest(struct CstxHandle *handle, + struct CstxSlice source, + struct CstxSlice data, + uint64_t *affected, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_node(struct CstxHandle *handle, + struct CstxSlice node_id, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_contains(struct CstxHandle *handle, + struct CstxSlice node_id, + uint8_t *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_stats(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_nodes(struct CstxHandle *handle, + struct CstxSlice filter_json, + struct CstxSlice options_json, + struct CstxNodeIterator **output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_edges(struct CstxHandle *handle, + struct CstxSlice filter_json, + struct CstxSlice options_json, + struct CstxEdgeIterator **output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_neighbors(struct CstxHandle *handle, + struct CstxSlice node_id, + struct CstxSlice direction, + struct CstxSlice options_json, + struct CstxNodeIterator **output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_query(struct CstxHandle *handle, + struct CstxSlice expression, + struct CstxSlice options_json, + struct CstxNodeIterator **output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_commit(struct CstxHandle *handle, + struct CstxSlice ref_name, + struct CstxSlice message, + struct CstxSlice metadata_json, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_diff(struct CstxHandle *handle, + struct CstxSlice base_ref, + struct CstxSlice head_ref, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_dump(struct CstxHandle *handle, + struct CstxSlice compression, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_load(struct CstxHandle *handle, + struct CstxSlice data, + struct CstxSlice compression, + uint64_t *consumed, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_dump_json(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_load_json(struct CstxHandle *handle, + struct CstxSlice data, + uint64_t *consumed, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_snapshot_fingerprint(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_head(struct CstxHandle *handle, + struct CstxSlice ref_name, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_refs(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); #endif /* CSTX_FFI_H */ diff --git a/go/cstx_native_test.go b/go/cstx_native_test.go new file mode 100644 index 0000000..347374c --- /dev/null +++ b/go/cstx_native_test.go @@ -0,0 +1,408 @@ +//go:build cstx_native + +package cstx + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "testing" +) + +var testContext = context.Background() + +var domainSchema = map[string]any{"properties": map[string]any{"domain": map[string]any{"type": "string"}}} + +func openRuntime(t *testing.T) *CSTX { + t.Helper() + rt, err := Open(testContext, Config{ProjectID: "sdk-go-test"}) + if err != nil { + t.Fatalf("open: %v", err) + } + t.Cleanup(func() { + if err := rt.Close(); err != nil { + t.Fatalf("close: %v", err) + } + }) + if err := rt.Schemas.Register(testContext, "domain", domainSchema, "domain"); err != nil { + t.Fatalf("register schema: %v", err) + } + return rt +} + +func domainNode(value string) Node { + return Node{ + ID: "domain:" + value, + Type: "domain", + Value: value, + Model: map[string]any{"domain": value, "cstx_flags": 0}, + Sources: []string{"test"}, + Extras: map[string]any{}, + } +} + +func relatedEdge(source, target string) Edge { + return Edge{ + ID: "relationship:" + source + ":related:" + target, + SourceID: source, + TargetID: target, + RelationType: "related", + Sources: []string{"test"}, + Attrs: map[string]any{}, + } +} + +func addDomain(t *testing.T, rt *CSTX, value string) uint64 { + t.Helper() + affected, err := rt.Graph.AddNodes(testContext, []Node{domainNode(value)}) + if err != nil { + t.Fatalf("add node %s: %v", value, err) + } + return affected +} + +func TestSchemas(t *testing.T) { + rt := openRuntime(t) + + contains, err := rt.Schemas.Contains(testContext, "domain") + if err != nil || !contains { + t.Fatalf("contains: %v %v", contains, err) + } + schema, err := rt.Schemas.Get(testContext, "domain") + if err != nil { + t.Fatalf("get: %v", err) + } + if schema["node_type"] != "domain" || schema["value_field"] != "domain" { + t.Fatalf("unexpected schema: %v", schema) + } + if _, ok := schema["schema"].(map[string]any)["properties"]; !ok { + t.Fatalf("unexpected schema body: %v", schema) + } + list, err := rt.Schemas.List(testContext) + if err != nil || len(list) == 0 { + t.Fatalf("list: %v %v", list, err) + } + if _, err := rt.Schemas.AvailablePlugins(testContext); err != nil { + t.Fatalf("available plugins: %v", err) + } +} + +func TestGraphMutationAndCursors(t *testing.T) { + rt := openRuntime(t) + + if affected := addDomain(t, rt, "example.com"); affected != 1 { + t.Fatalf("first add affected=%d", affected) + } + addDomain(t, rt, "www.example.com") + + if ok, err := rt.Graph.Contains(testContext, "domain:example.com"); err != nil || !ok { + t.Fatalf("contains: %v %v", ok, err) + } + if count, err := rt.Graph.NodeCount(testContext); err != nil || count != 2 { + t.Fatalf("node count: %v %v", count, err) + } + + node, err := rt.Graph.Node(testContext, "domain:example.com") + if err != nil { + t.Fatalf("node: %v", err) + } + if node.ID != "domain:example.com" || node.Type != "domain" { + t.Fatalf("unexpected node: %+v", node) + } + if node.Model["domain"] != "example.com" { + t.Fatalf("unexpected model: %+v", node.Model) + } + + if _, err := rt.Graph.Node(testContext, "domain:missing.example"); !IsCode(err, CodeNotFound) { + t.Fatalf("expected NOT_FOUND, got %v", err) + } + + // Re-adding identical content is a no-op: zero affected and live cursors + // stay valid. + cursor, err := rt.Graph.Nodes(testContext, NodeFilter{Types: []string{"domain"}}, CollectionOptions{Order: OrderIDAsc}) + if err != nil { + t.Fatalf("nodes: %v", err) + } + if affected := addDomain(t, rt, "example.com"); affected != 0 { + t.Fatalf("no-op add affected=%d", affected) + } + + var ids []string + for cursor.Next(testContext) { + ids = append(ids, cursor.Node().ID) + } + if err := cursor.Err(); err != nil { + t.Fatalf("cursor: %v", err) + } + if err := cursor.Close(); err != nil { + t.Fatalf("cursor close: %v", err) + } + if len(ids) != 2 || ids[0] != "domain:example.com" { + t.Fatalf("unexpected ids: %v", ids) + } + + stats, err := rt.Graph.Stats(testContext) + if err != nil { + t.Fatalf("stats: %v", err) + } + if stats.Nodes["domain"] != 2 { + t.Fatalf("unexpected stats: %+v", stats) + } +} + +func TestCursorInvalidation(t *testing.T) { + rt := openRuntime(t) + addDomain(t, rt, "example.com") + + cursor, err := rt.Graph.Nodes(testContext, NodeFilter{}, CollectionOptions{}) + if err != nil { + t.Fatalf("nodes: %v", err) + } + defer cursor.Close() + if !cursor.Next(testContext) { + t.Fatalf("expected first node, err=%v", cursor.Err()) + } + addDomain(t, rt, "other.example.com") + if cursor.Next(testContext) { + t.Fatal("expected cursor to stop after mutation") + } + if !IsCode(cursor.Err(), CodeCursorInvalidated) { + t.Fatalf("expected CURSOR_INVALIDATED, got %v", cursor.Err()) + } +} + +func TestGraphEdgesNeighborsAndQuery(t *testing.T) { + rt := openRuntime(t) + + addDomain(t, rt, "example.com") + addDomain(t, rt, "www.example.com") + affected, err := rt.Graph.AddEdges(testContext, []Edge{relatedEdge("domain:www.example.com", "domain:example.com")}) + if err != nil { + t.Fatalf("add edge: %v", err) + } + if affected != 1 { + t.Fatalf("add edge affected=%d", affected) + } + if count, err := rt.Graph.EdgeCount(testContext); err != nil || count != 1 { + t.Fatalf("edge count: %v %v", count, err) + } + + edges, err := rt.Graph.Edges(testContext, EdgeFilter{SourceID: "domain:www.example.com"}, CollectionOptions{}) + if err != nil { + t.Fatalf("edges: %v", err) + } + defer edges.Close() + if !edges.Next(testContext) { + t.Fatalf("expected one edge, err=%v", edges.Err()) + } + if edges.Edge().RelationType != "related" || edges.Edge().TargetID != "domain:example.com" { + t.Fatalf("unexpected edge: %+v", edges.Edge()) + } + if edges.Next(testContext) { + t.Fatal("expected exactly one edge") + } + + neighbors, err := rt.Graph.Neighbors(testContext, "domain:www.example.com", "out", CollectionOptions{}) + if err != nil { + t.Fatalf("neighbors: %v", err) + } + defer neighbors.Close() + if !neighbors.Next(testContext) || neighbors.Node().ID != "domain:example.com" { + t.Fatalf("unexpected neighbor, err=%v", neighbors.Err()) + } + + matches, err := rt.Graph.Query(testContext, "domain", QueryOptions{}) + if err != nil { + t.Fatalf("query: %v", err) + } + defer matches.Close() + count := 0 + for matches.Next(testContext) { + count++ + } + if err := matches.Err(); err != nil { + t.Fatalf("query cursor: %v", err) + } + if count != 2 { + t.Fatalf("expected 2 query matches, got %d", count) + } +} + +func TestRepositoryRoundTrip(t *testing.T) { + rt := openRuntime(t) + + addDomain(t, rt, "example.com") + commit, err := rt.Repo.Commit(testContext, "main", "initial", map[string]any{"origin": "test"}) + if err != nil { + t.Fatalf("commit: %v", err) + } + if commit.ID == "" || commit.Tree == "" { + t.Fatalf("unexpected commit: %+v", commit) + } + + head, err := rt.Repo.Head(testContext, "main") + if err != nil || head == nil || *head != commit.ID { + t.Fatalf("head: %v %v", head, err) + } + refs, err := rt.Repo.Refs(testContext) + if err != nil || len(refs) != 1 || refs[0].Name != "main" { + t.Fatalf("refs: %+v %v", refs, err) + } + + addDomain(t, rt, "www.example.com") + if _, err := rt.Repo.Commit(testContext, "main", "second", nil); err != nil { + t.Fatalf("second commit: %v", err) + } + + snapshot, err := rt.Repo.DumpJSON(testContext) + if err != nil || len(snapshot) == 0 { + t.Fatalf("dump json: %v", err) + } + fingerprint, err := rt.Repo.SnapshotFingerprint(testContext) + if err != nil || fingerprint == "" { + t.Fatalf("fingerprint: %v", err) + } + + compact, err := rt.Repo.Dump(testContext, "zstd1") + if err != nil || len(compact) == 0 { + t.Fatalf("dump: %v", err) + } + + rt2 := openRuntime(t) + consumed, err := rt2.Repo.LoadJSON(testContext, snapshot) + if err != nil { + t.Fatalf("load json: %v", err) + } + if consumed != uint64(len(snapshot)) { + t.Fatalf("consumed %d != %d", consumed, len(snapshot)) + } + if count, _ := rt2.Graph.NodeCount(testContext); count != 2 { + t.Fatalf("loaded node count: %d", count) + } + if fp, _ := rt2.Repo.SnapshotFingerprint(testContext); fp != fingerprint { + t.Fatalf("fingerprint mismatch after load: %s != %s", fp, fingerprint) + } + + rt3 := openRuntime(t) + if _, err := rt3.Repo.Load(testContext, compact, "zstd1"); err != nil { + t.Fatalf("load: %v", err) + } + if count, _ := rt3.Graph.NodeCount(testContext); count != 2 { + t.Fatalf("loaded compact node count: %d", count) + } +} + +func TestLastChange(t *testing.T) { + rt := openRuntime(t) + addDomain(t, rt, "example.com") + + change, err := rt.LastChange(testContext) + if err != nil { + t.Fatalf("last change: %v", err) + } + if len(change.AddedNodeIDs) != 1 || change.Affected() != 1 { + t.Fatalf("unexpected change set: %+v", change) + } +} + +func TestServicesAndCursorsHonorCanceledContext(t *testing.T) { + rt := openRuntime(t) + addDomain(t, rt, "example.com") + canceled, cancel := context.WithCancel(testContext) + cancel() + if _, err := rt.Graph.NodeCount(canceled); !errors.Is(err, context.Canceled) { + t.Fatalf("expected canceled service call, got %v", err) + } + + cursor, err := rt.Graph.Nodes(testContext, NodeFilter{}, CollectionOptions{}) + if err != nil { + t.Fatalf("nodes: %v", err) + } + defer cursor.Close() + if cursor.Next(canceled) { + t.Fatal("canceled cursor unexpectedly yielded a node") + } + if !errors.Is(cursor.Err(), context.Canceled) { + t.Fatalf("expected canceled cursor error, got %v", cursor.Err()) + } +} + +func TestCanonicalV03FixtureMatchesGoContract(t *testing.T) { + var fixture struct { + Schema struct { + NodeType string `json:"node_type"` + JSONSchema map[string]any `json:"json_schema"` + ValueField string `json:"value_field"` + } `json:"schema"` + Nodes []Node `json:"nodes"` + Edges []Edge `json:"edges"` + Query string `json:"query"` + Expected struct { + NodeIDs []string `json:"node_ids"` + NodeCount uint64 `json:"node_count"` + EdgeCount uint64 `json:"edge_count"` + } `json:"expected"` + } + payload, err := os.ReadFile(filepath.Join("..", "..", "tests", "fixtures", "v03_conformance.json")) + if err != nil { + t.Fatalf("read fixture: %v", err) + } + if err := json.Unmarshal(payload, &fixture); err != nil { + t.Fatalf("decode fixture: %v", err) + } + + rt, err := Open(testContext, Config{}) + if err != nil { + t.Fatalf("open: %v", err) + } + defer rt.Close() + if err := rt.Schemas.Register( + testContext, + fixture.Schema.NodeType, + fixture.Schema.JSONSchema, + fixture.Schema.ValueField, + ); err != nil { + t.Fatalf("register: %v", err) + } + if _, err := rt.Graph.AddNodes(testContext, fixture.Nodes); err != nil { + t.Fatalf("add nodes: %v", err) + } + if _, err := rt.Graph.AddEdges(testContext, fixture.Edges); err != nil { + t.Fatalf("add edges: %v", err) + } + if count, err := rt.Graph.NodeCount(testContext); err != nil || count != fixture.Expected.NodeCount { + t.Fatalf("node count: got %d err=%v", count, err) + } + if count, err := rt.Graph.EdgeCount(testContext); err != nil || count != fixture.Expected.EdgeCount { + t.Fatalf("edge count: got %d err=%v", count, err) + } + cursor, err := rt.Graph.Query(testContext, fixture.Query, QueryOptions{}) + if err != nil { + t.Fatalf("query: %v", err) + } + defer cursor.Close() + var ids []string + for cursor.Next(testContext) { + ids = append(ids, cursor.Node().ID) + } + if err := cursor.Err(); err != nil { + t.Fatalf("iterate query: %v", err) + } + if stringSliceMismatch(ids, fixture.Expected.NodeIDs) { + t.Fatalf("query IDs: got %v want %v", ids, fixture.Expected.NodeIDs) + } +} + +func stringSliceMismatch(left, right []string) bool { + if len(left) != len(right) { + return true + } + for i := range left { + if left[i] != right[i] { + return true + } + } + return false +} diff --git a/go/cstx_stub_test.go b/go/cstx_stub_test.go new file mode 100644 index 0000000..8de90d4 --- /dev/null +++ b/go/cstx_stub_test.go @@ -0,0 +1,25 @@ +//go:build !cstx_native + +package cstx + +import ( + "context" + "errors" + "testing" +) + +func TestOpenWithoutNativeTag(t *testing.T) { + rt, err := Open(context.Background(), Config{}) + if !errors.Is(err, ErrNativeUnavailable) { + t.Fatalf("expected ErrNativeUnavailable, got rt=%v err=%v", rt, err) + } +} + +func TestOpenHonorsCanceledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + rt, err := Open(ctx, Config{}) + if !errors.Is(err, context.Canceled) { + t.Fatalf("expected context.Canceled, got rt=%v err=%v", rt, err) + } +} diff --git a/go/cstx_test.go b/go/cstx_test.go new file mode 100644 index 0000000..1b54e81 --- /dev/null +++ b/go/cstx_test.go @@ -0,0 +1,94 @@ +package cstx + +import ( + "encoding/json" + "testing" +) + +func TestConfigNormalize(t *testing.T) { + cfg := Config{}.normalize() + if cfg.ProjectID != "default" { + t.Fatalf("project id: %q", cfg.ProjectID) + } + if cfg.CursorPageSize != DefaultCursorPageSize { + t.Fatalf("page size: %d", cfg.CursorPageSize) + } +} + +func TestNodeMarshalKeepsEmptyLists(t *testing.T) { + data, err := json.Marshal(Node{ID: "n1", Type: "Domain", Value: "example.com"}) + if err != nil { + t.Fatal(err) + } + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + for _, field := range []string{"sources", "model", "extras"} { + if decoded[field] == nil { + t.Fatalf("field %s must not be null: %s", field, data) + } + } +} + +func TestEdgeFilterNullIDs(t *testing.T) { + data, err := json.Marshal(EdgeFilter{}) + if err != nil { + t.Fatal(err) + } + var decoded map[string]any + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + for _, field := range []string{"source_id", "target_id"} { + value, present := decoded[field] + if !present || value != nil { + t.Fatalf("field %s must be explicit null: %s", field, data) + } + } + for _, field := range []string{"relations", "sources"} { + if decoded[field] == nil { + t.Fatalf("field %s must be []: %s", field, data) + } + } +} + +func TestCollectionOptionsNormalize(t *testing.T) { + options := CollectionOptions{}.normalize() + if options.PageSize != DefaultCursorPageSize || options.Order != OrderUnspecified { + t.Fatalf("unexpected defaults: %+v", options) + } +} + +func TestRefUnmarshalTuple(t *testing.T) { + var refs []Ref + if err := json.Unmarshal([]byte(`[["main","abc123"],["dev","def456"]]`), &refs); err != nil { + t.Fatal(err) + } + if len(refs) != 2 || refs[0].Name != "main" || refs[0].Head != "abc123" { + t.Fatalf("unexpected refs: %+v", refs) + } +} + +func TestParseErrorRoundTrip(t *testing.T) { + raw := []byte(`{"code":"NOT_FOUND","operation":"graph.node","item_index":null,"field":"node_id","message":"missing","expected":null,"actual":null}`) + cerr := parseError(raw, CodeInternal) + if cerr.Code != CodeNotFound || cerr.Operation != "graph.node" || cerr.Field != "node_id" { + t.Fatalf("unexpected error: %+v", cerr) + } + if !IsCode(cerr, CodeNotFound) { + t.Fatal("IsCode mismatch") + } + + fallback := parseError([]byte("plain failure"), CodeIO) + if fallback.Code != CodeIO || fallback.Message != "plain failure" { + t.Fatalf("unexpected fallback: %+v", fallback) + } +} + +func TestChangeSetAffected(t *testing.T) { + change := ChangeSet{AddedNodeIDs: []string{"a"}, UpdatedEdgeIDs: []string{"b", "c"}} + if change.Affected() != 3 { + t.Fatalf("affected: %d", change.Affected()) + } +} diff --git a/go/cursor.go b/go/cursor.go new file mode 100644 index 0000000..89872f7 --- /dev/null +++ b/go/cursor.go @@ -0,0 +1,99 @@ +package cstx + +import "context" + +// NodeCursor is a lazy node iterator that avoids materializing a full result +// set. Callers must Close it; Close is idempotent. A mutation that +// invalidates the cursor surfaces through Err as CodeCursorInvalidated. +type NodeCursor struct { + iter nodeIter + node Node + err error + done bool +} + +// Next advances the cursor. It returns false at exhaustion or on error; +// check Err after the loop. +func (c *NodeCursor) Next(ctx context.Context) bool { + if c.done { + return false + } + if err := contextError(ctx); err != nil { + c.err = err + c.done = true + return false + } + node, ok, err := c.iter.next(ctx) + if err != nil { + c.err = err + c.done = true + return false + } + if !ok { + c.done = true + return false + } + c.node = node + return true +} + +// Node returns the current node. It is valid only after Next returned true. +func (c *NodeCursor) Node() Node { return c.node } + +// Err returns the first error encountered during iteration. +func (c *NodeCursor) Err() error { return c.err } + +// Close releases cursor state; repeated calls are safe. +func (c *NodeCursor) Close() error { + if !c.done { + c.done = true + c.iter.close() + } + return nil +} + +// EdgeCursor is a lazy relationship iterator. +type EdgeCursor struct { + iter edgeIter + edge Edge + err error + done bool +} + +func (c *EdgeCursor) Next(ctx context.Context) bool { + if c.done { + return false + } + if err := contextError(ctx); err != nil { + c.err = err + c.done = true + return false + } + edge, ok, err := c.iter.next(ctx) + if err != nil { + c.err = err + c.done = true + return false + } + if !ok { + c.done = true + return false + } + c.edge = edge + return true +} + +// Edge returns the current relationship. +func (c *EdgeCursor) Edge() Edge { return c.edge } + +// Err returns the first error encountered during iteration. +func (c *EdgeCursor) Err() error { return c.err } + +// Close releases cursor state; repeated calls are safe. +func (c *EdgeCursor) Close() error { + if !c.done { + c.done = true + c.iter.close() + } + return nil +} diff --git a/go/doc.go b/go/doc.go new file mode 100644 index 0000000..783a9b4 --- /dev/null +++ b/go/doc.go @@ -0,0 +1,16 @@ +// Package cstx is the typed Go facade for the CSTX v0.3 native runtime. +// +// The Rust core is the sole owner of graph, repository, validation, +// and ingest semantics (issue #6). This package only converts between Go +// domain values and the JSON transport used by the cstx-ffi C boundary; it +// never reimplements business behavior. +// +// The engine behind Open requires the cstx-ffi static library and is enabled +// with the cstx_native build tag: +// +// go build -tags cstx_native +// +// Without the tag the package still compiles and all types remain available, +// but Open returns ErrNativeUnavailable. Prebuilt libraries live under +// lib// and are synced from the cstx release pipeline. +package cstx diff --git a/go/engine.go b/go/engine.go new file mode 100644 index 0000000..306072b --- /dev/null +++ b/go/engine.go @@ -0,0 +1,56 @@ +package cstx + +import "context" + +// engine is the internal boundary between the typed facade and the transport +// implementation. The native build implements it over the cstx-ffi C ABI; the +// stub build returns ErrNativeUnavailable. Neither layer owns semantics. +type engine interface { + close() error + + lastChange(context.Context) (ChangeSet, error) + + schemaRegister(context.Context, string, map[string]any, string) error + schemaContains(context.Context, string) (bool, error) + schemaGet(context.Context, string) (map[string]any, error) + schemaList(context.Context) ([]map[string]any, error) + schemaLoadPlugin(context.Context, string) error + schemaLoadAllPlugins(context.Context) error + schemaAvailablePlugins(context.Context) ([]string, error) + + graphAddNodes(context.Context, []Node) (uint64, error) + graphAddEdges(context.Context, []Edge) (uint64, error) + graphIngest(context.Context, string, []byte) (uint64, error) + graphNode(context.Context, string) (Node, error) + graphContains(context.Context, string) (bool, error) + graphNodeCount(context.Context) (uint64, error) + graphEdgeCount(context.Context) (uint64, error) + graphStats(context.Context) (GraphStats, error) + graphNodes(context.Context, NodeFilter, CollectionOptions) (nodeIter, error) + graphEdges(context.Context, EdgeFilter, CollectionOptions) (edgeIter, error) + graphNeighbors(context.Context, string, string, CollectionOptions) (nodeIter, error) + graphQuery(context.Context, string, QueryOptions) (nodeIter, error) + + repoCommit(context.Context, string, string, any) (Commit, error) + repoDiff(context.Context, string, string) (GraphDiff, error) + repoDump(context.Context, string) ([]byte, error) + repoLoad(context.Context, []byte, string) (uint64, error) + repoDumpJSON(context.Context) ([]byte, error) + repoLoadJSON(context.Context, []byte) (uint64, error) + repoSnapshotFingerprint(context.Context) (string, error) + repoHead(context.Context, string) (*string, error) + repoRefs(context.Context) ([]Ref, error) + +} + +// nodeIter yields one node at a time from the native runtime. ok is false at +// exhaustion; close is idempotent. +type nodeIter interface { + next(context.Context) (node Node, ok bool, err error) + close() +} + +type edgeIter interface { + next(context.Context) (edge Edge, ok bool, err error) + close() +} diff --git a/go/engine_native.go b/go/engine_native.go new file mode 100644 index 0000000..694dcdf --- /dev/null +++ b/go/engine_native.go @@ -0,0 +1,561 @@ +//go:build cstx_native + +package cstx + +/* +#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/lib/linux_amd64 -lcstx_ffi -lm -ldl -lpthread +#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/lib/linux_arm64 -lcstx_ffi -lm -ldl -lpthread +#cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/lib/darwin_amd64 -lcstx_ffi -lm -framework Security -framework CoreFoundation +#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/lib/darwin_arm64 -lcstx_ffi -lm -framework Security -framework CoreFoundation +#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/lib/windows_amd64 -lcstx_ffi -lws2_32 -luserenv -lbcrypt -lntdll -ladvapi32 +#include "cstx_ffi.h" +#include +*/ +import "C" + +import ( + "context" + "encoding/json" + "runtime" + "unsafe" +) + +type nativeEngine struct { + handle *C.CstxHandle +} + +func newEngine(config Config) (engine, error) { + payload, err := json.Marshal(map[string]any{ + "project_id": config.ProjectID, + "cursor_page_size": config.CursorPageSize, + }) + if err != nil { + return nil, err + } + var handle *C.CstxHandle + err = statusCall("cstx.open", func(errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_open(byteSlice(payload), &handle, errBuf) + runtime.KeepAlive(payload) + return rc + }) + if err != nil { + return nil, err + } + eng := &nativeEngine{handle: handle} + runtime.SetFinalizer(eng, (*nativeEngine).finalize) + return eng, nil +} + +func (e *nativeEngine) finalize() { _ = e.close() } + +func (e *nativeEngine) close() error { + if e.handle != nil { + C.cstx_free(e.handle) + e.handle = nil + runtime.SetFinalizer(e, nil) + } + return nil +} + +// --- C transport helpers ------------------------------------------------- + +var emptySliceByte byte + +func byteSlice(value []byte) C.CstxSlice { + if len(value) == 0 { + return C.CstxSlice{} + } + return C.CstxSlice{data: (*C.uint8_t)(unsafe.Pointer(&value[0])), len: C.size_t(len(value))} +} + +func stringSlice(value string) C.CstxSlice { + if len(value) == 0 { + return C.CstxSlice{} + } + return C.CstxSlice{data: (*C.uint8_t)(unsafe.Pointer(unsafe.StringData(value))), len: C.size_t(len(value))} +} + +// optionalStringSlice maps "" to a null slice (Rust None). A non-empty value +// is borrowed for the duration of the call. +func optionalStringSlice(value string) C.CstxSlice { + if value == "" { + return C.CstxSlice{} + } + return stringSlice(value) +} + +func statusCodeFallback(code C.CstxStatusCode) Code { + switch code { + case C.CSTX_INVALID_ARGUMENT: + return CodeInvalidArgument + case C.CSTX_NOT_FOUND: + return CodeNotFound + case C.CSTX_CONFLICT: + return CodeConflict + case C.CSTX_NOT_INITIALIZED: + return CodeNotInitialized + case C.CSTX_UNSUPPORTED: + return CodeUnsupported + case C.CSTX_VALIDATION: + return CodeValidation + case C.CSTX_PARSE: + return CodeParse + case C.CSTX_IO: + return CodeIO + case C.CSTX_CORRUPT_DATA: + return CodeCorruptData + case C.CSTX_CURSOR_INVALIDATED: + return CodeCursorInvalidated + default: + return CodeInternal + } +} + +func statusError(rc C.CstxStatusCode, op string, errBuf *C.CstxBuffer) error { + defer C.cstx_buffer_free(errBuf) + if rc == C.CSTX_OK { + return nil + } + cerr := parseError(takeBuffer(errBuf), statusCodeFallback(rc)) + if cerr.Operation == "" { + cerr.Operation = op + } + return cerr +} + +func takeBuffer(buffer *C.CstxBuffer) []byte { + defer C.cstx_buffer_free(buffer) + if buffer.data == nil || buffer.len == 0 { + return []byte{} + } + source := unsafe.Slice((*byte)(unsafe.Pointer(buffer.data)), int(buffer.len)) + result := make([]byte, len(source)) + copy(result, source) + return result +} + +func statusCall(op string, call func(errBuf *C.CstxBuffer) C.CstxStatusCode) error { + var errBuf C.CstxBuffer + return statusError(call(&errBuf), op, &errBuf) +} + +// jsonResult runs a call whose success output is a JSON buffer and decodes it +// into result. +func jsonResult(op string, result any, call func(out, errBuf *C.CstxBuffer) C.CstxStatusCode) error { + var out, errBuf C.CstxBuffer + if err := statusError(call(&out, &errBuf), op, &errBuf); err != nil { + C.cstx_buffer_free(&out) + return err + } + return json.Unmarshal(takeBuffer(&out), result) +} + +func countResult(op string, call func(out *C.uint64_t, errBuf *C.CstxBuffer) C.CstxStatusCode) (uint64, error) { + var out C.uint64_t + var errBuf C.CstxBuffer + if err := statusError(call(&out, &errBuf), op, &errBuf); err != nil { + return 0, err + } + return uint64(out), nil +} + +func boolResult(op string, call func(out *C.uint8_t, errBuf *C.CstxBuffer) C.CstxStatusCode) (bool, error) { + var out C.uint8_t + var errBuf C.CstxBuffer + if err := statusError(call(&out, &errBuf), op, &errBuf); err != nil { + return false, err + } + return out != 0, nil +} + +func marshal(value any) []byte { + data, err := json.Marshal(value) + if err != nil { + // All marshaled types are internal contracts; a failure here is a bug. + panic("cstx: marshal transport value: " + err.Error()) + } + return data +} + +func marshalInput(op string, value any) ([]byte, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, &Error{Code: CodeInvalidArgument, Operation: op, Message: err.Error()} + } + return data, nil +} + +// --- runtime ------------------------------------------------------------- + +func (e *nativeEngine) lastChange(_ context.Context) (ChangeSet, error) { + var change ChangeSet + err := jsonResult("cstx.last_change", &change, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_last_change_json(e.handle, out, errBuf) + }) + return change, err +} + +// --- schemas ------------------------------------------------------------- + +func (e *nativeEngine) schemaRegister(_ context.Context, nodeType string, schema map[string]any, valueField string) error { + payload, err := marshalInput("schemas.register", schema) + if err != nil { + return err + } + return statusCall("schemas.register", func(errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_schema_register(e.handle, stringSlice(nodeType), byteSlice(payload), optionalStringSlice(valueField), errBuf) + runtime.KeepAlive(nodeType) + runtime.KeepAlive(payload) + runtime.KeepAlive(valueField) + return rc + }) +} + +func (e *nativeEngine) schemaContains(_ context.Context, nodeType string) (bool, error) { + return boolResult("schemas.contains", func(out *C.uint8_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_schema_contains(e.handle, stringSlice(nodeType), out, errBuf) + runtime.KeepAlive(nodeType) + return rc + }) +} + +func (e *nativeEngine) schemaGet(_ context.Context, nodeType string) (map[string]any, error) { + var schema map[string]any + err := jsonResult("schemas.get", &schema, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_schema_get_json(e.handle, stringSlice(nodeType), out, errBuf) + runtime.KeepAlive(nodeType) + return rc + }) + return schema, err +} + +func (e *nativeEngine) schemaList(_ context.Context) ([]map[string]any, error) { + var schemas []map[string]any + err := jsonResult("schemas.list", &schemas, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_schema_list_json(e.handle, out, errBuf) + }) + return schemas, err +} + +func (e *nativeEngine) schemaLoadPlugin(_ context.Context, name string) error { + return statusCall("schemas.load_plugin", func(errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_schema_load_plugin(e.handle, stringSlice(name), errBuf) + runtime.KeepAlive(name) + return rc + }) +} + +func (e *nativeEngine) schemaLoadAllPlugins(_ context.Context) error { + return statusCall("schemas.load_all_plugins", func(errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_schema_load_all_plugins(e.handle, errBuf) + }) +} + +func (e *nativeEngine) schemaAvailablePlugins(_ context.Context) ([]string, error) { + var plugins []string + err := jsonResult("schemas.available_plugins", &plugins, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_schema_available_plugins_json(e.handle, out, errBuf) + }) + return plugins, err +} + +// --- graph --------------------------------------------------------------- + +func (e *nativeEngine) graphAddNodes(_ context.Context, nodes []Node) (uint64, error) { + payload := marshal(nodes) + return countResult("graph.add_nodes", func(out *C.uint64_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_add_nodes(e.handle, byteSlice(payload), out, errBuf) + runtime.KeepAlive(payload) + return rc + }) +} + +func (e *nativeEngine) graphAddEdges(_ context.Context, edges []Edge) (uint64, error) { + payload := marshal(edges) + return countResult("graph.add_edges", func(out *C.uint64_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_add_edges(e.handle, byteSlice(payload), out, errBuf) + runtime.KeepAlive(payload) + return rc + }) +} + +func (e *nativeEngine) graphIngest(_ context.Context, source string, data []byte) (uint64, error) { + return countResult("graph.ingest", func(out *C.uint64_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_ingest(e.handle, stringSlice(source), byteSlice(data), out, errBuf) + runtime.KeepAlive(source) + runtime.KeepAlive(data) + return rc + }) +} + +func (e *nativeEngine) graphNode(_ context.Context, nodeID string) (Node, error) { + var node Node + err := jsonResult("graph.node", &node, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_node(e.handle, stringSlice(nodeID), out, errBuf) + runtime.KeepAlive(nodeID) + return rc + }) + return node, err +} + +func (e *nativeEngine) graphContains(_ context.Context, nodeID string) (bool, error) { + return boolResult("graph.contains", func(out *C.uint8_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_contains(e.handle, stringSlice(nodeID), out, errBuf) + runtime.KeepAlive(nodeID) + return rc + }) +} + +func (e *nativeEngine) graphNodeCount(_ context.Context) (uint64, error) { + return countResult("graph.node_count", func(out *C.uint64_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_graph_node_count(e.handle, out, errBuf) + }) +} + +func (e *nativeEngine) graphEdgeCount(_ context.Context) (uint64, error) { + return countResult("graph.edge_count", func(out *C.uint64_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_graph_edge_count(e.handle, out, errBuf) + }) +} + +func (e *nativeEngine) graphStats(_ context.Context) (GraphStats, error) { + var stats GraphStats + err := jsonResult("graph.stats", &stats, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_graph_stats(e.handle, out, errBuf) + }) + return stats, err +} + +func (e *nativeEngine) graphNodes(_ context.Context, filter NodeFilter, options CollectionOptions) (nodeIter, error) { + filterJSON := marshal(filter) + optionsJSON := marshal(options) + var iter *C.CstxNodeIterator + err := statusCall("graph.nodes", func(errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_nodes(e.handle, byteSlice(filterJSON), byteSlice(optionsJSON), &iter, errBuf) + runtime.KeepAlive(filterJSON) + runtime.KeepAlive(optionsJSON) + return rc + }) + if err != nil { + return nil, err + } + return newNodeIter(iter), nil +} + +func (e *nativeEngine) graphEdges(_ context.Context, filter EdgeFilter, options CollectionOptions) (edgeIter, error) { + filterJSON := marshal(filter) + optionsJSON := marshal(options) + var iter *C.CstxEdgeIterator + err := statusCall("graph.edges", func(errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_edges(e.handle, byteSlice(filterJSON), byteSlice(optionsJSON), &iter, errBuf) + runtime.KeepAlive(filterJSON) + runtime.KeepAlive(optionsJSON) + return rc + }) + if err != nil { + return nil, err + } + return newEdgeIter(iter), nil +} + +func (e *nativeEngine) graphNeighbors(_ context.Context, nodeID, direction string, options CollectionOptions) (nodeIter, error) { + optionsJSON := marshal(options) + var iter *C.CstxNodeIterator + err := statusCall("graph.neighbors", func(errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_neighbors(e.handle, stringSlice(nodeID), stringSlice(direction), byteSlice(optionsJSON), &iter, errBuf) + runtime.KeepAlive(nodeID) + runtime.KeepAlive(direction) + runtime.KeepAlive(optionsJSON) + return rc + }) + if err != nil { + return nil, err + } + return newNodeIter(iter), nil +} + +func (e *nativeEngine) graphQuery(_ context.Context, expression string, options QueryOptions) (nodeIter, error) { + optionsJSON := marshal(options) + var iter *C.CstxNodeIterator + err := statusCall("graph.query", func(errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_graph_query(e.handle, stringSlice(expression), byteSlice(optionsJSON), &iter, errBuf) + runtime.KeepAlive(expression) + runtime.KeepAlive(optionsJSON) + return rc + }) + if err != nil { + return nil, err + } + return newNodeIter(iter), nil +} + +// --- repository ---------------------------------------------------------- + +func (e *nativeEngine) repoCommit(_ context.Context, refName, message string, metadata any) (Commit, error) { + var metadataJSON []byte + if metadata != nil { + var err error + metadataJSON, err = marshalInput("repo.commit", metadata) + if err != nil { + return Commit{}, err + } + } + var commit Commit + err := jsonResult("repo.commit", &commit, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_repo_commit(e.handle, stringSlice(refName), stringSlice(message), byteSlice(metadataJSON), out, errBuf) + runtime.KeepAlive(refName) + runtime.KeepAlive(message) + runtime.KeepAlive(metadataJSON) + return rc + }) + return commit, err +} + +func (e *nativeEngine) repoDiff(_ context.Context, baseRef, headRef string) (GraphDiff, error) { + var diff GraphDiff + err := jsonResult("repo.diff", &diff, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_repo_diff(e.handle, stringSlice(baseRef), stringSlice(headRef), out, errBuf) + runtime.KeepAlive(baseRef) + runtime.KeepAlive(headRef) + return rc + }) + return diff, err +} + +func (e *nativeEngine) repoDump(_ context.Context, compression string) ([]byte, error) { + var out, errBuf C.CstxBuffer + rc := C.cstx_repo_dump(e.handle, stringSlice(compression), &out, &errBuf) + runtime.KeepAlive(compression) + if err := statusError(rc, "repo.dump", &errBuf); err != nil { + C.cstx_buffer_free(&out) + return nil, err + } + return takeBuffer(&out), nil +} + +func (e *nativeEngine) repoLoad(_ context.Context, data []byte, compression string) (uint64, error) { + return countResult("repo.load", func(out *C.uint64_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_repo_load(e.handle, byteSlice(data), stringSlice(compression), out, errBuf) + runtime.KeepAlive(data) + runtime.KeepAlive(compression) + return rc + }) +} + +func (e *nativeEngine) repoDumpJSON(_ context.Context) ([]byte, error) { + var out, errBuf C.CstxBuffer + if err := statusError(C.cstx_repo_dump_json(e.handle, &out, &errBuf), "repo.dump_json", &errBuf); err != nil { + C.cstx_buffer_free(&out) + return nil, err + } + return takeBuffer(&out), nil +} + +func (e *nativeEngine) repoLoadJSON(_ context.Context, data []byte) (uint64, error) { + return countResult("repo.load_json", func(out *C.uint64_t, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_repo_load_json(e.handle, byteSlice(data), out, errBuf) + runtime.KeepAlive(data) + return rc + }) +} + +func (e *nativeEngine) repoSnapshotFingerprint(_ context.Context) (string, error) { + var fingerprint string + err := jsonResult("repo.snapshot_fingerprint", &fingerprint, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_repo_snapshot_fingerprint(e.handle, out, errBuf) + }) + return fingerprint, err +} + +func (e *nativeEngine) repoHead(_ context.Context, refName string) (*string, error) { + var head *string + err := jsonResult("repo.head", &head, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + rc := C.cstx_repo_head(e.handle, stringSlice(refName), out, errBuf) + runtime.KeepAlive(refName) + return rc + }) + return head, err +} + +func (e *nativeEngine) repoRefs(_ context.Context) ([]Ref, error) { + var refs []Ref + err := jsonResult("repo.refs", &refs, func(out, errBuf *C.CstxBuffer) C.CstxStatusCode { + return C.cstx_repo_refs(e.handle, out, errBuf) + }) + return refs, err +} + +// --- iterators ----------------------------------------------------------- +type nativeNodeIter struct{ iter *C.CstxNodeIterator } + +func newNodeIter(iter *C.CstxNodeIterator) *nativeNodeIter { + it := &nativeNodeIter{iter: iter} + runtime.SetFinalizer(it, (*nativeNodeIter).close) + return it +} + +func (i *nativeNodeIter) next(_ context.Context) (Node, bool, error) { + var node Node + if i.iter == nil { + return node, false, nil + } + var out, errBuf C.CstxBuffer + var hasValue C.uint8_t + if err := statusError(C.cstx_node_iterator_next(i.iter, &out, &hasValue, &errBuf), "cursor.next", &errBuf); err != nil { + C.cstx_buffer_free(&out) + return node, false, err + } + if hasValue == 0 { + C.cstx_buffer_free(&out) + return node, false, nil + } + if err := json.Unmarshal(takeBuffer(&out), &node); err != nil { + return node, false, err + } + return node, true, nil +} + +func (i *nativeNodeIter) close() { + if i.iter != nil { + C.cstx_node_iterator_free(i.iter) + i.iter = nil + runtime.SetFinalizer(i, nil) + } +} + +type nativeEdgeIter struct{ iter *C.CstxEdgeIterator } + +func newEdgeIter(iter *C.CstxEdgeIterator) *nativeEdgeIter { + it := &nativeEdgeIter{iter: iter} + runtime.SetFinalizer(it, (*nativeEdgeIter).close) + return it +} + +func (i *nativeEdgeIter) next(_ context.Context) (Edge, bool, error) { + var edge Edge + if i.iter == nil { + return edge, false, nil + } + var out, errBuf C.CstxBuffer + var hasValue C.uint8_t + if err := statusError(C.cstx_edge_iterator_next(i.iter, &out, &hasValue, &errBuf), "cursor.next", &errBuf); err != nil { + C.cstx_buffer_free(&out) + return edge, false, err + } + if hasValue == 0 { + C.cstx_buffer_free(&out) + return edge, false, nil + } + if err := json.Unmarshal(takeBuffer(&out), &edge); err != nil { + return edge, false, err + } + return edge, true, nil +} + +func (i *nativeEdgeIter) close() { + if i.iter != nil { + C.cstx_edge_iterator_free(i.iter) + i.iter = nil + runtime.SetFinalizer(i, nil) + } +} diff --git a/go/engine_stub.go b/go/engine_stub.go new file mode 100644 index 0000000..ebcf48a --- /dev/null +++ b/go/engine_stub.go @@ -0,0 +1,9 @@ +//go:build !cstx_native + +package cstx + +// newEngine without the cstx_native build tag: the package compiles and all +// types remain usable, but there is no runtime behind it. +func newEngine(Config) (engine, error) { + return nil, ErrNativeUnavailable +} diff --git a/go/errors.go b/go/errors.go new file mode 100644 index 0000000..54162f4 --- /dev/null +++ b/go/errors.go @@ -0,0 +1,89 @@ +package cstx + +import ( + "encoding/json" + "errors" + "fmt" +) + +// Code is the stable, language-neutral error category shared with the Rust +// core and every other binding. +type Code string + +const ( + CodeInvalidArgument Code = "INVALID_ARGUMENT" + CodeNotFound Code = "NOT_FOUND" + CodeConflict Code = "CONFLICT" + CodeValidation Code = "VALIDATION" + CodeParse Code = "PARSE" + CodeNotInitialized Code = "NOT_INITIALIZED" + CodeUnsupported Code = "UNSUPPORTED" + CodeIO Code = "IO" + CodeCorruptData Code = "CORRUPT_DATA" + CodeCursorInvalidated Code = "CURSOR_INVALIDATED" + CodeInternal Code = "INTERNAL" +) + +// ErrNativeUnavailable is returned by every engine operation when the package +// is compiled without the cstx_native build tag. +var ErrNativeUnavailable = errors.New("cstx: native engine not compiled (build with -tags cstx_native)") + +// Error is a stable CSTX failure whose fields can be handled without parsing +// text. It mirrors the Rust CstxError contract. +type Error struct { + Code Code `json:"code"` + Operation string `json:"operation"` + ItemIndex *int `json:"item_index"` + Field string `json:"field"` + Message string `json:"message"` + Expected string `json:"expected"` + Actual string `json:"actual"` +} + +func (e *Error) Error() string { + if e.Operation == "" { + return fmt.Sprintf("cstx: %s: %s", e.Code, e.Message) + } + return fmt.Sprintf("cstx: %s: %s: %s", e.Operation, e.Code, e.Message) +} + +// IsCode reports whether err is a CSTX *Error with the given code. +func IsCode(err error, code Code) bool { + var cerr *Error + return errors.As(err, &cerr) && cerr.Code == code +} + +// errorJSON is the FFI transport shape; nullable string fields distinguish +// absent context from empty strings on the Rust side. +type errorJSON struct { + Code Code `json:"code"` + Operation string `json:"operation"` + ItemIndex *int `json:"item_index"` + Field *string `json:"field"` + Message string `json:"message"` + Expected *string `json:"expected"` + Actual *string `json:"actual"` +} + +func parseError(data []byte, fallback Code) *Error { + var wire errorJSON + if err := json.Unmarshal(data, &wire); err != nil || wire.Code == "" { + return &Error{Code: fallback, Message: string(data)} + } + return &Error{ + Code: wire.Code, + Operation: wire.Operation, + ItemIndex: wire.ItemIndex, + Field: deref(wire.Field), + Message: wire.Message, + Expected: deref(wire.Expected), + Actual: deref(wire.Actual), + } +} + +func deref(value *string) string { + if value == nil { + return "" + } + return *value +} diff --git a/go/generate.go b/go/generate.go deleted file mode 100644 index 0ee1b82..0000000 --- a/go/generate.go +++ /dev/null @@ -1,3 +0,0 @@ -package cstx - -//go:generate sh ./scripts/download.sh diff --git a/go/go.mod b/go/go.mod index 234f9bf..5d51d8e 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,3 +1,3 @@ module github.com/chainreactors/libcstx/go -go 1.22.0 +go 1.25.0 diff --git a/go/graph.go b/go/graph.go index 9c7dbec..5006458 100644 --- a/go/graph.go +++ b/go/graph.go @@ -1,392 +1,124 @@ package cstx -/* -#include "cstx_ffi.h" -#include -*/ -import "C" -import ( - "encoding/json" - "fmt" - "unsafe" -) +import "context" -type Graph struct { - ptr *C.struct_CstxGraph -} - -func New() *Graph { - return &Graph{ptr: C.cstx_graph_new()} -} - -func (g *Graph) Close() { - if g.ptr != nil { - C.cstx_graph_free(g.ptr) - g.ptr = nil - } -} - -// ── Plugin loading ── - -func (g *Graph) LoadPlugin(name string) error { - cs := C.CString(name) - defer C.free(unsafe.Pointer(cs)) - if C.cstx_graph_load_plugin(g.ptr, cs) != 0 { - return fmt.Errorf("cstx: plugin %q not found", name) - } - return nil -} - -func (g *Graph) LoadAllPlugins() int { - return int(C.cstx_graph_load_all_plugins(g.ptr)) -} - -func AvailablePlugins() []string { - cs := C.cstx_available_plugins() - defer C.cstx_free_string(cs) - var out []string - json.Unmarshal([]byte(C.GoString(cs)), &out) - return out -} +// Graph is the graph namespace of a CSTX runtime. It owns graph data, graph +// queries, and ingest; the repository lifecycle lives elsewhere. +type Graph struct{ eng engine } -// ── Schema + Rules ── - -func (g *Graph) RegisterSchema(nodeType, jsonSchema string, valueField string) error { - cType := C.CString(nodeType) - cSchema := C.CString(jsonSchema) - defer C.free(unsafe.Pointer(cType)) - defer C.free(unsafe.Pointer(cSchema)) - - var cVF *C.char - if valueField != "" { - cVF = C.CString(valueField) - defer C.free(unsafe.Pointer(cVF)) +// AddNodes atomically adds or merges nodes and returns the number of elements +// actually changed. A no-op write reports zero and does not invalidate +// cursors. +func (g *Graph) AddNodes(ctx context.Context, nodes []Node) (uint64, error) { + if err := contextError(ctx); err != nil { + return 0, err } - if C.cstx_graph_register_schema(g.ptr, cType, cSchema, cVF) != 0 { - return fmt.Errorf("cstx: register schema %q failed", nodeType) - } - return nil + return g.eng.graphAddNodes(ctx, nodes) } -func (g *Graph) AddJoinRule(leftType, rightType, relation, leftKey, rightKey string, predicted bool) error { - clt := C.CString(leftType) - crt := C.CString(rightType) - cr := C.CString(relation) - clk := C.CString(leftKey) - crk := C.CString(rightKey) - defer C.free(unsafe.Pointer(clt)) - defer C.free(unsafe.Pointer(crt)) - defer C.free(unsafe.Pointer(cr)) - defer C.free(unsafe.Pointer(clk)) - defer C.free(unsafe.Pointer(crk)) - - pred := C.int(0) - if predicted { - pred = 1 +// AddEdges atomically adds or merges relationships. +func (g *Graph) AddEdges(ctx context.Context, edges []Edge) (uint64, error) { + if err := contextError(ctx); err != nil { + return 0, err } - if C.cstx_graph_add_join_rule(g.ptr, clt, crt, cr, clk, crk, pred, nil, nil) != 0 { - return fmt.Errorf("cstx: add join rule failed") - } - return nil + return g.eng.graphAddEdges(ctx, edges) } -// ── Node / Edge CRUD ── - -func (g *Graph) AddNode(nodeType, cstxID, payloadJSON string, sources []string, flags uint64) error { - cType := C.CString(nodeType) - cID := C.CString(cstxID) - cPayload := C.CString(payloadJSON) - srcJSON, _ := json.Marshal(sources) - cSrc := C.CString(string(srcJSON)) - defer C.free(unsafe.Pointer(cType)) - defer C.free(unsafe.Pointer(cID)) - defer C.free(unsafe.Pointer(cPayload)) - defer C.free(unsafe.Pointer(cSrc)) - - if C.cstx_graph_add_node(g.ptr, cType, cID, cPayload, cSrc, C.uint64_t(flags)) != 0 { - return fmt.Errorf("cstx: add node failed") +// Ingest feeds one linked native-plugin payload into the shared graph. +func (g *Graph) Ingest(ctx context.Context, source string, data []byte) (uint64, error) { + if err := contextError(ctx); err != nil { + return 0, err } - return nil + return g.eng.graphIngest(ctx, source, data) } -func (g *Graph) AddEdge(sourceID, targetID, relation, dataSource string) error { - cs := C.CString(sourceID) - ct := C.CString(targetID) - cr := C.CString(relation) - cd := C.CString(dataSource) - defer C.free(unsafe.Pointer(cs)) - defer C.free(unsafe.Pointer(ct)) - defer C.free(unsafe.Pointer(cr)) - defer C.free(unsafe.Pointer(cd)) - - if C.cstx_graph_add_edge(g.ptr, cs, ct, cr, cd) != 0 { - return fmt.Errorf("cstx: add edge failed") +// Node returns one node or a *Error with CodeNotFound. +func (g *Graph) Node(ctx context.Context, nodeID string) (Node, error) { + if err := contextError(ctx); err != nil { + return Node{}, err } - return nil + return g.eng.graphNode(ctx, nodeID) } -func (g *Graph) AddNodesBatch(nodesJSON string) (*BatchResult, error) { - cj := C.CString(nodesJSON) - defer C.free(unsafe.Pointer(cj)) - - var out *C.char - var outLen C.size_t - rc := C.cstx_graph_add_nodes_batch(g.ptr, cj, &out, &outLen) - if out != nil { - defer C.cstx_free_string(out) +// Contains reports node existence without materializing the node. +func (g *Graph) Contains(ctx context.Context, nodeID string) (bool, error) { + if err := contextError(ctx); err != nil { + return false, err } - if rc != 0 { - return nil, ffiError("add_nodes_batch", out, outLen) - } - var r BatchResult - json.Unmarshal([]byte(C.GoStringN(out, C.int(outLen))), &r) - return &r, nil + return g.eng.graphContains(ctx, nodeID) } -func (g *Graph) AddEdgesBatch(edgesJSON string) (int, error) { - cj := C.CString(edgesJSON) - defer C.free(unsafe.Pointer(cj)) - rc := C.cstx_graph_add_edges_batch(g.ptr, cj) - if rc < 0 { - return 0, fmt.Errorf("cstx: add_edges_batch failed") +// NodeCount returns the current number of nodes. +func (g *Graph) NodeCount(ctx context.Context) (uint64, error) { + if err := contextError(ctx); err != nil { + return 0, err } - return int(rc), nil + return g.eng.graphNodeCount(ctx) } -// ── Ingest ── - -func (g *Graph) Link(source string, data []byte) (*IngestResult, error) { - cs := C.CString(source) - defer C.free(unsafe.Pointer(cs)) - - var dp *C.uchar - if len(data) > 0 { - dp = (*C.uchar)(unsafe.Pointer(&data[0])) - } - var out *C.char - var outLen C.size_t - rc := C.cstx_graph_link(g.ptr, cs, dp, C.size_t(len(data)), &out, &outLen) - if out != nil { - defer C.cstx_free_string(out) +// EdgeCount returns the current number of relationships. +func (g *Graph) EdgeCount(ctx context.Context) (uint64, error) { + if err := contextError(ctx); err != nil { + return 0, err } - if rc != 0 { - return nil, ffiError("link", out, outLen) - } - var r IngestResult - json.Unmarshal([]byte(C.GoStringN(out, C.int(outLen))), &r) - return &r, nil + return g.eng.graphEdgeCount(ctx) } -func (g *Graph) IngestNative(plugin, artifact string, data []byte) (*IngestResult, error) { - cp := C.CString(plugin) - ca := C.CString(artifact) - defer C.free(unsafe.Pointer(cp)) - defer C.free(unsafe.Pointer(ca)) - - var dp *C.uchar - if len(data) > 0 { - dp = (*C.uchar)(unsafe.Pointer(&data[0])) - } - var out *C.char - var outLen C.size_t - rc := C.cstx_graph_ingest_native(g.ptr, cp, ca, dp, C.size_t(len(data)), &out, &outLen) - if out != nil { - defer C.cstx_free_string(out) +// Stats returns small aggregate counts. +func (g *Graph) Stats(ctx context.Context) (GraphStats, error) { + if err := contextError(ctx); err != nil { + return GraphStats{}, err } - if rc != 0 { - return nil, ffiError("ingest_native", out, outLen) - } - var r IngestResult - json.Unmarshal([]byte(C.GoStringN(out, C.int(outLen))), &r) - return &r, nil + return g.eng.graphStats(ctx) } -func (g *Graph) HasNativeArtifact(artifact string) bool { - ca := C.CString(artifact) - defer C.free(unsafe.Pointer(ca)) - return C.cstx_graph_has_native_artifact(g.ptr, ca) != 0 -} - -func (g *Graph) IngestJSONL(nodeType string, data []byte, idExpr, dataSource string) (*IngestResult, error) { - cType := C.CString(nodeType) - cExpr := C.CString(idExpr) - cSrc := C.CString(dataSource) - defer C.free(unsafe.Pointer(cType)) - defer C.free(unsafe.Pointer(cExpr)) - defer C.free(unsafe.Pointer(cSrc)) - - var dp *C.uchar - if len(data) > 0 { - dp = (*C.uchar)(unsafe.Pointer(&data[0])) - } - var out *C.char - var outLen C.size_t - rc := C.cstx_graph_ingest_jsonl(g.ptr, cType, dp, C.size_t(len(data)), cExpr, cSrc, &out, &outLen) - if out != nil { - defer C.cstx_free_string(out) - } - if rc != 0 { - return nil, ffiError("ingest_jsonl", out, outLen) +// Nodes creates a lazy cursor over nodes matching the filter. The zero +// filter and options select everything with runtime defaults. +func (g *Graph) Nodes(ctx context.Context, filter NodeFilter, options CollectionOptions) (*NodeCursor, error) { + if err := contextError(ctx); err != nil { + return nil, err } - var r IngestResult - json.Unmarshal([]byte(C.GoStringN(out, C.int(outLen))), &r) - return &r, nil -} - -func (g *Graph) LinkNodes(dataSource string) (string, error) { - cs := C.CString(dataSource) - defer C.free(unsafe.Pointer(cs)) - var out *C.char - var outLen C.size_t - C.cstx_graph_link_nodes(g.ptr, cs, &out, &outLen) - if out != nil { - defer C.cstx_free_string(out) - return C.GoStringN(out, C.int(outLen)), nil + iter, err := g.eng.graphNodes(ctx, filter, options.normalize()) + if err != nil { + return nil, err } - return "{}", nil + return &NodeCursor{iter: iter}, nil } -// ── Queries ── - -func (g *Graph) Node(nodeID string) (string, bool) { - cs := C.CString(nodeID) - defer C.free(unsafe.Pointer(cs)) - out := C.cstx_graph_node(g.ptr, cs) - if out == nil { - return "", false +// Edges creates a lazy cursor over relationships matching the filter. +func (g *Graph) Edges(ctx context.Context, filter EdgeFilter, options CollectionOptions) (*EdgeCursor, error) { + if err := contextError(ctx); err != nil { + return nil, err } - defer C.cstx_free_string(out) - return C.GoString(out), true -} - -func (g *Graph) Nodes(typeFilter string) string { - var cf *C.char - if typeFilter != "" { - cf = C.CString(typeFilter) - defer C.free(unsafe.Pointer(cf)) + iter, err := g.eng.graphEdges(ctx, filter, options.normalize()) + if err != nil { + return nil, err } - out := C.cstx_graph_nodes(g.ptr, cf) - defer C.cstx_free_string(out) - return C.GoString(out) + return &EdgeCursor{iter: iter}, nil } -func (g *Graph) Edges(relation string) string { - var cr *C.char - if relation != "" { - cr = C.CString(relation) - defer C.free(unsafe.Pointer(cr)) +// Neighbors lazily traverses neighboring nodes. Direction is "out", "in", +// or "both". +func (g *Graph) Neighbors(ctx context.Context, nodeID, direction string, options CollectionOptions) (*NodeCursor, error) { + if err := contextError(ctx); err != nil { + return nil, err } - out := C.cstx_graph_edges(g.ptr, cr) - defer C.cstx_free_string(out) - return C.GoString(out) -} - -func (g *Graph) NodeTypes() []string { - out := C.cstx_graph_node_types(g.ptr) - defer C.cstx_free_string(out) - var types []string - json.Unmarshal([]byte(C.GoString(out)), &types) - return types -} - -func (g *Graph) Neighbors(nodeID string) string { - cs := C.CString(nodeID) - defer C.free(unsafe.Pointer(cs)) - out := C.cstx_graph_neighbors(g.ptr, cs) - defer C.cstx_free_string(out) - return C.GoString(out) -} - -func (g *Graph) ContainsNode(nodeID string) bool { - cs := C.CString(nodeID) - defer C.free(unsafe.Pointer(cs)) - return C.cstx_graph_contains_node(g.ptr, cs) != 0 -} - -func (g *Graph) NodeCount() int { - return int(C.cstx_graph_node_count(g.ptr)) -} - -func (g *Graph) EdgeCount() int { - return int(C.cstx_graph_edge_count(g.ptr)) -} - -func (g *Graph) NodeIDs() []string { - out := C.cstx_graph_node_ids(g.ptr) - defer C.cstx_free_string(out) - var ids []string - json.Unmarshal([]byte(C.GoString(out)), &ids) - return ids -} - -func (g *Graph) Stats() string { - out := C.cstx_graph_stats(g.ptr) - defer C.cstx_free_string(out) - return C.GoString(out) -} - -func (g *Graph) ToJSON() string { - out := C.cstx_graph_to_json(g.ptr) - defer C.cstx_free_string(out) - return C.GoString(out) -} - -func (g *Graph) AllNodesJSON() string { - out := C.cstx_graph_all_nodes_json(g.ptr) - defer C.cstx_free_string(out) - return C.GoString(out) -} - -func (g *Graph) NodePayload(nodeID string) (string, bool) { - cs := C.CString(nodeID) - defer C.free(unsafe.Pointer(cs)) - out := C.cstx_graph_node_payload(g.ptr, cs) - if out == nil { - return "", false + iter, err := g.eng.graphNeighbors(ctx, nodeID, direction, options.normalize()) + if err != nil { + return nil, err } - defer C.cstx_free_string(out) - return C.GoString(out), true + return &NodeCursor{iter: iter}, nil } -// ── Stateless transform (compatible with utils/cstx Parse) ── - -func Transform(sourceType string, data []byte) ([]byte, error) { - cs := C.CString(sourceType) - defer C.free(unsafe.Pointer(cs)) - - var dp *C.uchar - if len(data) > 0 { - dp = (*C.uchar)(unsafe.Pointer(&data[0])) +// Query executes the graph DSL and returns a lazy cursor over terminal nodes. +func (g *Graph) Query(ctx context.Context, expression string, options QueryOptions) (*NodeCursor, error) { + if err := contextError(ctx); err != nil { + return nil, err } - var out *C.char - var outLen C.size_t - rc := C.cstx_transform(cs, dp, C.size_t(len(data)), &out, &outLen) - if out != nil { - defer C.cstx_free_string(out) - } - if rc != 0 { - return nil, ffiError("transform", out, outLen) - } - return C.GoBytes(unsafe.Pointer(out), C.int(outLen)), nil -} - -// ── Utilities ── - -func Version() string { - return C.GoString(C.cstx_version()) -} - -func SupportedArtifacts() []string { - out := C.cstx_supported_artifacts() - defer C.cstx_free_string(out) - var arts []string - json.Unmarshal([]byte(C.GoString(out)), &arts) - return arts -} - -func ffiError(op string, out *C.char, outLen C.size_t) error { - if out != nil { - return fmt.Errorf("cstx %s: %s", op, C.GoStringN(out, C.int(outLen))) + options.Collection = options.Collection.normalize() + iter, err := g.eng.graphQuery(ctx, expression, options) + if err != nil { + return nil, err } - return fmt.Errorf("cstx %s failed", op) + return &NodeCursor{iter: iter}, nil } diff --git a/go/graph_filter.go b/go/graph_filter.go deleted file mode 100644 index c8c323f..0000000 --- a/go/graph_filter.go +++ /dev/null @@ -1,59 +0,0 @@ -package cstx - -/* -#include "cstx_ffi.h" -#include -*/ -import "C" -import "unsafe" - -// EdgesFiltered returns edges matching optional filters. Empty string means no filter. -func (g *Graph) EdgesFiltered(sourceID, targetID, relation string) string { - var cSrc, cTgt, cRel *C.char - if sourceID != "" { - cSrc = C.CString(sourceID) - defer C.free(unsafe.Pointer(cSrc)) - } - if targetID != "" { - cTgt = C.CString(targetID) - defer C.free(unsafe.Pointer(cTgt)) - } - if relation != "" { - cRel = C.CString(relation) - defer C.free(unsafe.Pointer(cRel)) - } - - out := C.cstx_graph_edges_filtered(g.ptr, cSrc, cTgt, cRel) - if out == nil { - return "[]" - } - defer C.cstx_free_string(out) - return C.GoString(out) -} - -// ExportSnapshotJSON exports a complete graph snapshot as JSON. -func (g *Graph) ExportSnapshotJSON() string { - out := C.cstx_graph_export_snapshot_json(g.ptr) - if out == nil { - return "{}" - } - defer C.cstx_free_string(out) - return C.GoString(out) -} - -// UpdateNodeFlags modifies flags on a node. setTo=-1 means don't set absolute value. -func (g *Graph) UpdateNodeFlags(nodeID string, add, remove uint64, setTo int64) bool { - cID := C.CString(nodeID) - defer C.free(unsafe.Pointer(cID)) - return C.cstx_graph_update_node_flags(g.ptr, cID, C.uint64_t(add), C.uint64_t(remove), C.int64_t(setTo)) == 0 -} - -// FlagsAllMask returns the mask of all defined flags. -func FlagsAllMask() uint64 { - return uint64(C.cstx_flags_all_mask()) -} - -// FlagsDefaultExcludeMask returns the default exclude mask for queries. -func FlagsDefaultExcludeMask() uint64 { - return uint64(C.cstx_flags_default_exclude_mask()) -} diff --git a/go/graph_query.go b/go/graph_query.go deleted file mode 100644 index d599f7e..0000000 --- a/go/graph_query.go +++ /dev/null @@ -1,50 +0,0 @@ -package cstx - -/* -#include "cstx_ffi.h" -#include -*/ -import "C" -import ( - "encoding/json" - "unsafe" -) - -// QueryDSL executes a DSL expression and returns results as JSON. -func (g *Graph) QueryDSL(expression string, limit int, offset int) (string, error) { - cExpr := C.CString(expression) - defer C.free(unsafe.Pointer(cExpr)) - - var out *C.char - var outLen C.uintptr_t - rc := C.cstx_graph_query_dsl(g.ptr, cExpr, C.intptr_t(limit), C.uintptr_t(offset), &out, &outLen) - if out != nil { - defer C.cstx_free_string(out) - } - if rc != 0 { - return "", ffiError("query_dsl", out, outLen) - } - return C.GoStringN(out, C.int(outLen)), nil -} - -// QueryNodeIDs executes a DSL expression and returns matching node IDs. -func (g *Graph) QueryNodeIDs(expression string, limit int, offset int) ([]string, error) { - cExpr := C.CString(expression) - defer C.free(unsafe.Pointer(cExpr)) - - out := C.cstx_graph_query_node_ids(g.ptr, cExpr, C.intptr_t(limit), C.uintptr_t(offset)) - if out == nil { - return nil, nil - } - defer C.cstx_free_string(out) - var ids []string - json.Unmarshal([]byte(C.GoString(out)), &ids) - return ids, nil -} - -// IsPathExpression returns whether the expression is a path/traversal query. -func (g *Graph) IsPathExpression(expression string) bool { - cExpr := C.CString(expression) - defer C.free(unsafe.Pointer(cExpr)) - return C.cstx_graph_is_path_expression(g.ptr, cExpr) != 0 -} diff --git a/go/graph_test.go b/go/graph_test.go deleted file mode 100644 index 7fdf715..0000000 --- a/go/graph_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package cstx - -import "testing" - -func TestGraphLifecycle(t *testing.T) { - g := New() - defer g.Close() - - n := g.LoadAllPlugins() - if n == 0 { - t.Fatal("expected at least 1 plugin loaded") - } - t.Logf("loaded %d plugins", n) - - if g.NodeCount() != 0 { - t.Error("new graph should have 0 nodes") - } -} - -func TestGraphAddNode(t *testing.T) { - g := New() - defer g.Close() - g.LoadAllPlugins() - - err := g.AddNode("ip", "ip:10.0.0.1", `{"ip":"10.0.0.1","country":"CN"}`, []string{"test"}, 0) - if err != nil { - t.Fatal(err) - } - if g.NodeCount() != 1 { - t.Errorf("expected 1 node, got %d", g.NodeCount()) - } - if !g.ContainsNode("ip:10.0.0.1") { - t.Error("node should exist") - } -} - -func TestGraphIngestNative(t *testing.T) { - g := New() - defer g.Close() - g.LoadAllPlugins() - - data := []byte(`{"host":"www.example.com","a":["1.2.3.4"],"ttl":300}` + "\n") - result, err := g.IngestNative("easm", "dnsx", data) - if err != nil { - t.Fatal(err) - } - if result.RecordsParsed == 0 { - t.Error("expected records_parsed > 0") - } - t.Logf("ingest: %+v", result) -} - -func TestTransform(t *testing.T) { - data := []byte(`{"host":"www.example.com","a":["1.2.3.4"],"ttl":300}` + "\n") - nodes, err := Parse("dnsx", data) - if err != nil { - t.Fatal(err) - } - if len(nodes) == 0 { - t.Fatal("expected nodes") - } - for _, n := range nodes { - t.Logf(" %s %s", n.CstxType(), n.CstxID()) - } -} - -func TestVersion(t *testing.T) { - v := Version() - if v == "" || v == "stub" { - t.Error("expected non-empty version") - } - t.Logf("version: %s", v) -} - -func TestSupportedArtifacts(t *testing.T) { - arts := SupportedArtifacts() - if len(arts) == 0 { - t.Fatal("expected artifacts") - } - t.Logf("artifacts: %v", arts) -} diff --git a/go/graph_traversal.go b/go/graph_traversal.go deleted file mode 100644 index 7e251a7..0000000 --- a/go/graph_traversal.go +++ /dev/null @@ -1,98 +0,0 @@ -package cstx - -/* -#include "cstx_ffi.h" -#include -*/ -import "C" -import ( - "encoding/json" - "unsafe" -) - -// NeighborIDs returns IDs of neighbors in the given direction ("in", "out", "both"). -func (g *Graph) NeighborIDs(nodeID string, direction string) []string { - cID := C.CString(nodeID) - cDir := C.CString(direction) - defer C.free(unsafe.Pointer(cID)) - defer C.free(unsafe.Pointer(cDir)) - - out := C.cstx_graph_neighbor_ids(g.ptr, cID, cDir) - if out == nil { - return nil - } - defer C.cstx_free_string(out) - var ids []string - json.Unmarshal([]byte(C.GoString(out)), &ids) - return ids -} - -// BFS performs breadth-first search from seed. depth=0 means unlimited. -func (g *Graph) BFS(seedID string, depth uint32, reverse bool) []string { - cSeed := C.CString(seedID) - defer C.free(unsafe.Pointer(cSeed)) - - rev := C.int(0) - if reverse { - rev = 1 - } - out := C.cstx_graph_bfs(g.ptr, cSeed, C.uint(depth), rev) - if out == nil { - return nil - } - defer C.cstx_free_string(out) - var ids []string - json.Unmarshal([]byte(C.GoString(out)), &ids) - return ids -} - -// ShortestPaths returns all shortest paths between two nodes. -func (g *Graph) ShortestPaths(startID string, endID string, maxDepth uint32) [][]string { - cStart := C.CString(startID) - cEnd := C.CString(endID) - defer C.free(unsafe.Pointer(cStart)) - defer C.free(unsafe.Pointer(cEnd)) - - out := C.cstx_graph_shortest_paths(g.ptr, cStart, cEnd, C.uint(maxDepth)) - if out == nil { - return nil - } - defer C.cstx_free_string(out) - var paths [][]string - json.Unmarshal([]byte(C.GoString(out)), &paths) - return paths -} - -// Degree returns node degree in the given direction. -func (g *Graph) Degree(nodeID string, direction string) int { - cID := C.CString(nodeID) - cDir := C.CString(direction) - defer C.free(unsafe.Pointer(cID)) - defer C.free(unsafe.Pointer(cDir)) - - return int(C.cstx_graph_degree(g.ptr, cID, cDir)) -} - -// SubgraphNodeIDs returns node and edge IDs reachable from seeds within the given depth. -func (g *Graph) SubgraphNodeIDs(seedIDs []string, depth uint32) (nodeIDs []string, edgeIDs []string, err error) { - seedJSON, _ := json.Marshal(seedIDs) - cSeeds := C.CString(string(seedJSON)) - defer C.free(unsafe.Pointer(cSeeds)) - - var out *C.char - var outLen C.size_t - rc := C.cstx_graph_subgraph_node_ids(g.ptr, cSeeds, C.uint(depth), &out, &outLen) - if out != nil { - defer C.cstx_free_string(out) - } - if rc != 0 { - return nil, nil, ffiError("subgraph_node_ids", out, outLen) - } - - var result struct { - Nodes []string `json:"nodes"` - Edges []string `json:"edges"` - } - json.Unmarshal([]byte(C.GoStringN(out, C.int(outLen))), &result) - return result.Nodes, result.Edges, nil -} diff --git a/go/lib/darwin_amd64/libcstx_ffi.a b/go/lib/darwin_amd64/libcstx_ffi.a index 502480c..ec56b1f 100644 Binary files a/go/lib/darwin_amd64/libcstx_ffi.a and b/go/lib/darwin_amd64/libcstx_ffi.a differ diff --git a/go/lib/darwin_arm64/libcstx_ffi.a b/go/lib/darwin_arm64/libcstx_ffi.a index 37a43ca..b4d75bd 100644 Binary files a/go/lib/darwin_arm64/libcstx_ffi.a and b/go/lib/darwin_arm64/libcstx_ffi.a differ diff --git a/go/lib/linux_amd64/libcstx_ffi.a b/go/lib/linux_amd64/libcstx_ffi.a index 4923a79..b20abd3 100644 Binary files a/go/lib/linux_amd64/libcstx_ffi.a and b/go/lib/linux_amd64/libcstx_ffi.a differ diff --git a/go/lib/linux_arm64/libcstx_ffi.a b/go/lib/linux_arm64/libcstx_ffi.a index 11b0c24..0d519f4 100644 Binary files a/go/lib/linux_arm64/libcstx_ffi.a and b/go/lib/linux_arm64/libcstx_ffi.a differ diff --git a/go/lib/windows_amd64/libcstx_ffi.a b/go/lib/windows_amd64/libcstx_ffi.a index e54e37e..ef68213 100644 Binary files a/go/lib/windows_amd64/libcstx_ffi.a and b/go/lib/windows_amd64/libcstx_ffi.a differ diff --git a/go/options.go b/go/options.go new file mode 100644 index 0000000..7775cb7 --- /dev/null +++ b/go/options.go @@ -0,0 +1,87 @@ +package cstx + +import "encoding/json" + +// DefaultCursorPageSize matches the Rust runtime default. +const DefaultCursorPageSize = 1024 + +// NodeFilter pushes node selection into Rust before values are materialized. +type NodeFilter struct { + Types []string `json:"types"` + IDs []string `json:"ids"` + Sources []string `json:"sources"` + FlagsAll uint64 `json:"flags_all"` + FlagsAny uint64 `json:"flags_any"` + FlagsNone uint64 `json:"flags_none"` +} + +func (f NodeFilter) MarshalJSON() ([]byte, error) { + type wire NodeFilter + f.Types = orEmpty(f.Types) + f.IDs = orEmpty(f.IDs) + f.Sources = orEmpty(f.Sources) + return json.Marshal(wire(f)) +} + +// EdgeFilter pushes relationship selection into Rust. +type EdgeFilter struct { + SourceID string `json:"source_id"` + TargetID string `json:"target_id"` + Relations []string `json:"relations"` + Sources []string `json:"sources"` +} + +func (f EdgeFilter) MarshalJSON() ([]byte, error) { + // source_id/target_id are Option in Rust: absent means null, + // and Some("") would filter on an empty ID instead of disabling it. + type wire struct { + SourceID *string `json:"source_id"` + TargetID *string `json:"target_id"` + Relations []string `json:"relations"` + Sources []string `json:"sources"` + } + return json.Marshal(wire{ + SourceID: nonEmpty(f.SourceID), + TargetID: nonEmpty(f.TargetID), + Relations: orEmpty(f.Relations), + Sources: orEmpty(f.Sources), + }) +} + +// CollectionOptions bounds cursor materialization and ordering. +// The zero value uses the runtime defaults (no limit, page size 1024, +// unspecified order). +type CollectionOptions struct { + Limit *int `json:"limit"` + PageSize int `json:"page_size"` + Order Order `json:"order"` +} + +func (o CollectionOptions) normalize() CollectionOptions { + if o.PageSize <= 0 { + o.PageSize = DefaultCursorPageSize + } + if o.Order == "" { + o.Order = OrderUnspecified + } + return o +} + +// QueryOptions applies collection semantics to graph DSL queries. +type QueryOptions struct { + Collection CollectionOptions `json:"collection"` +} + +func orEmpty(value []string) []string { + if value == nil { + return []string{} + } + return value +} + +func nonEmpty(value string) *string { + if value == "" { + return nil + } + return &value +} diff --git a/go/repository.go b/go/repository.go new file mode 100644 index 0000000..1587186 --- /dev/null +++ b/go/repository.go @@ -0,0 +1,81 @@ +package cstx + +import "context" + +// Repository is the snapshot/version namespace of a CSTX runtime. +type Repository struct{ eng engine } + +// Commit writes the current graph content to a named repository ref. +func (r *Repository) Commit(ctx context.Context, refName, message string, metadata any) (Commit, error) { + if err := contextError(ctx); err != nil { + return Commit{}, err + } + return r.eng.repoCommit(ctx, refName, message, metadata) +} + +// Diff returns added, removed, and modified IDs between two refs. +func (r *Repository) Diff(ctx context.Context, baseRef, headRef string) (GraphDiff, error) { + if err := contextError(ctx); err != nil { + return GraphDiff{}, err + } + return r.eng.repoDiff(ctx, baseRef, headRef) +} + +// Dump returns a compact snapshot byte container. Compression is for example +// "zstd1"; an empty string selects the runtime default. +func (r *Repository) Dump(ctx context.Context, compression string) ([]byte, error) { + if err := contextError(ctx); err != nil { + return nil, err + } + return r.eng.repoDump(ctx, compression) +} + +// Load replaces graph state from a compact snapshot and reports the number +// of bytes consumed. +func (r *Repository) Load(ctx context.Context, data []byte, compression string) (uint64, error) { + if err := contextError(ctx); err != nil { + return 0, err + } + return r.eng.repoLoad(ctx, data, compression) +} + +// DumpJSON returns canonical JSON snapshot bytes for interoperability. +func (r *Repository) DumpJSON(ctx context.Context) ([]byte, error) { + if err := contextError(ctx); err != nil { + return nil, err + } + return r.eng.repoDumpJSON(ctx) +} + +// LoadJSON loads canonical JSON snapshot bytes and reports bytes consumed. +func (r *Repository) LoadJSON(ctx context.Context, data []byte) (uint64, error) { + if err := contextError(ctx); err != nil { + return 0, err + } + return r.eng.repoLoadJSON(ctx, data) +} + +// SnapshotFingerprint returns a stable hash of the canonical snapshot. +func (r *Repository) SnapshotFingerprint(ctx context.Context) (string, error) { + if err := contextError(ctx); err != nil { + return "", err + } + return r.eng.repoSnapshotFingerprint(ctx) +} + +// Head returns the commit ID of one named ref, or nil when the ref does not +// exist. +func (r *Repository) Head(ctx context.Context, refName string) (*string, error) { + if err := contextError(ctx); err != nil { + return nil, err + } + return r.eng.repoHead(ctx, refName) +} + +// Refs lists repository refs without exposing Merkle internals. +func (r *Repository) Refs(ctx context.Context) ([]Ref, error) { + if err := contextError(ctx); err != nil { + return nil, err + } + return r.eng.repoRefs(ctx) +} diff --git a/go/schemas.go b/go/schemas.go new file mode 100644 index 0000000..78ab61f --- /dev/null +++ b/go/schemas.go @@ -0,0 +1,63 @@ +package cstx + +import "context" + +// Schemas is the schema/plugin namespace of a CSTX runtime. +type Schemas struct{ eng engine } + +// Register adds canonical validation metadata for one node type. An empty +// valueField means no designated value field. +func (s *Schemas) Register(ctx context.Context, nodeType string, schema map[string]any, valueField string) error { + if err := contextError(ctx); err != nil { + return err + } + return s.eng.schemaRegister(ctx, nodeType, schema, valueField) +} + +// Contains reports whether a schema exists for the node type. +func (s *Schemas) Contains(ctx context.Context, nodeType string) (bool, error) { + if err := contextError(ctx); err != nil { + return false, err + } + return s.eng.schemaContains(ctx, nodeType) +} + +// Get returns one retained schema. +func (s *Schemas) Get(ctx context.Context, nodeType string) (map[string]any, error) { + if err := contextError(ctx); err != nil { + return nil, err + } + return s.eng.schemaGet(ctx, nodeType) +} + +// List returns retained schemas in deterministic node-type order. +func (s *Schemas) List(ctx context.Context) ([]map[string]any, error) { + if err := contextError(ctx); err != nil { + return nil, err + } + return s.eng.schemaList(ctx) +} + +// LoadPlugin loads one linked native plugin into the shared graph engine. +func (s *Schemas) LoadPlugin(ctx context.Context, name string) error { + if err := contextError(ctx); err != nil { + return err + } + return s.eng.schemaLoadPlugin(ctx, name) +} + +// LoadAllPlugins loads every linked native plugin. +func (s *Schemas) LoadAllPlugins(ctx context.Context) error { + if err := contextError(ctx); err != nil { + return err + } + return s.eng.schemaLoadAllPlugins(ctx) +} + +// AvailablePlugins lists linked plugins without changing runtime state. +func (s *Schemas) AvailablePlugins(ctx context.Context) ([]string, error) { + if err := contextError(ctx); err != nil { + return nil, err + } + return s.eng.schemaAvailablePlugins(ctx) +} diff --git a/go/sco_easm.go b/go/sco_easm.go new file mode 100644 index 0000000..a505d05 --- /dev/null +++ b/go/sco_easm.go @@ -0,0 +1,331 @@ +// @generated by cstx-codegen — DO NOT EDIT. + +package cstx + +import "encoding/json" + +type DomainNode struct { + nodeHeader + Host string `json:"host"` +} + +type SubdomainNode struct { + nodeHeader + Host string `json:"host"` + IsTld bool `json:"is_tld,omitempty"` + Ttl int64 `json:"ttl,omitempty"` + Resolver []string `json:"resolver,omitempty"` + A []string `json:"a,omitempty"` + Aaaa []string `json:"aaaa,omitempty"` + Cname []string `json:"cname,omitempty"` + Mx []string `json:"mx,omitempty"` + Ns []string `json:"ns,omitempty"` + Txt []string `json:"txt,omitempty"` +} + +type IpNode struct { + nodeHeader + Ip string `json:"ip"` + Country string `json:"country,omitempty"` + Area string `json:"area,omitempty"` + AsnNumber string `json:"asn_number,omitempty"` + AsName string `json:"as_name,omitempty"` + CdnName string `json:"cdn_name,omitempty"` + CloudName string `json:"cloud_name,omitempty"` + WafName string `json:"waf_name,omitempty"` + Cdn bool `json:"cdn,omitempty"` + Cloud bool `json:"cloud,omitempty"` + Waf bool `json:"waf,omitempty"` +} + +type CidrNode struct { + nodeHeader + Cidr string `json:"cidr"` +} + +type PortNode struct { + nodeHeader + Ip string `json:"ip"` + Port string `json:"port"` + Protocol string `json:"protocol"` +} + +type AppNode struct { + nodeHeader + AppId string `json:"app_id"` + Url string `json:"url,omitempty"` + Frameworks []string `json:"frameworks,omitempty"` + Title string `json:"title,omitempty"` + Midware string `json:"midware,omitempty"` + Status string `json:"status,omitempty"` + StatusCode int64 `json:"status_code,omitempty"` + Host string `json:"host,omitempty"` + ContentType string `json:"content_type,omitempty"` + BodyLength int64 `json:"body_length,omitempty"` + HeaderLength int64 `json:"header_length,omitempty"` + ScreenshotId string `json:"screenshot_id,omitempty"` + ScreenshotPath string `json:"screenshot_path,omitempty"` + Ip string `json:"ip,omitempty"` + Port string `json:"port,omitempty"` +} + +type UrlNode struct { + nodeHeader + Scheme string `json:"scheme"` + Host string `json:"host,omitempty"` + Port string `json:"port,omitempty"` + Path string `json:"path,omitempty"` + Ip string `json:"ip,omitempty"` + StatusCode int64 `json:"status_code,omitempty"` + Title string `json:"title,omitempty"` + BodyLength int64 `json:"body_length,omitempty"` + ContentType string `json:"content_type,omitempty"` + RedirectUrl string `json:"redirect_url,omitempty"` + Frameworks []string `json:"frameworks,omitempty"` +} + +type FrameworkNode struct { + nodeHeader + Name string `json:"name"` + Part string `json:"part,omitempty"` + Vendor string `json:"vendor,omitempty"` + Product string `json:"product,omitempty"` + Version string `json:"version,omitempty"` + Tags []string `json:"tags,omitempty"` + IsFocus bool `json:"is_focus,omitempty"` + Sources []string `json:"sources,omitempty"` +} + +type VulnNode struct { + nodeHeader + Value string `json:"value"` + VulnId string `json:"vuln_id,omitempty"` + Name string `json:"name,omitempty"` + AssetId string `json:"asset_id,omitempty"` + Severity string `json:"severity,omitempty"` + Tags []string `json:"tags,omitempty"` + Ip string `json:"ip,omitempty"` + Host string `json:"host,omitempty"` + Port string `json:"port,omitempty"` + Protocol string `json:"protocol,omitempty"` + Scheme string `json:"scheme,omitempty"` + Url string `json:"url,omitempty"` + Path string `json:"path,omitempty"` + Pocname string `json:"pocname,omitempty"` + Request string `json:"request,omitempty"` + Response string `json:"response,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Matched bool `json:"matched,omitempty"` + Extracted bool `json:"extracted,omitempty"` +} + +type SarifVulnNode struct { + nodeHeader + Value string `json:"value"` + VulnId string `json:"vuln_id,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Source string `json:"source,omitempty"` + Target string `json:"target,omitempty"` + Tags []string `json:"tags,omitempty"` + AssetCstxId string `json:"asset_cstx_id,omitempty"` + Kind string `json:"kind,omitempty"` + Level string `json:"level,omitempty"` + BaselineState string `json:"baseline_state,omitempty"` + RuleId string `json:"rule_id,omitempty"` + Evidence string `json:"evidence,omitempty"` +} + +type CertificateNode struct { + nodeHeader + Fingerprint string `json:"fingerprint"` + Serial string `json:"serial,omitempty"` + Issuer string `json:"issuer,omitempty"` + Subject string `json:"subject,omitempty"` + NotBefore string `json:"not_before,omitempty"` + NotAfter string `json:"not_after,omitempty"` + San []string `json:"san,omitempty"` + Host string `json:"host,omitempty"` + Ip string `json:"ip,omitempty"` +} + +type CompanyNode struct { + nodeHeader + Name string `json:"name"` + Perc string `json:"perc,omitempty"` + Tycid string `json:"tycid,omitempty"` + Icp string `json:"icp,omitempty"` + Parent string `json:"parent,omitempty"` +} + +type IcpNode struct { + nodeHeader + Icp string `json:"icp"` + Sub string `json:"sub,omitempty"` + Date string `json:"date,omitempty"` + Company string `json:"company,omitempty"` + Title string `json:"title,omitempty"` + Domain string `json:"domain,omitempty"` + Ip string `json:"ip,omitempty"` +} + +type BucketNode struct { + nodeHeader + Provider string `json:"provider,omitempty"` + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + Endpoint string `json:"endpoint"` + Acl string `json:"acl,omitempty"` + ObjectCount int64 `json:"object_count,omitempty"` + KnownPaths []string `json:"known_paths,omitempty"` + SourceUrl string `json:"source_url,omitempty"` +} + +type EndpointNode struct { + nodeHeader + Url string `json:"url"` + Method string `json:"method,omitempty"` + Path string `json:"path,omitempty"` + ContentType string `json:"content_type,omitempty"` + StatusCode int64 `json:"status_code,omitempty"` + Source string `json:"source,omitempty"` + SourceUrl string `json:"source_url,omitempty"` + Parameters []string `json:"parameters,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +type HostNode struct { + nodeHeader + Hostname string `json:"hostname"` + LocalIps []string `json:"local_ips,omitempty"` + GatewayIps []string `json:"gateway_ips,omitempty"` + DnsServers []string `json:"dns_servers,omitempty"` + DomainName string `json:"domain_name,omitempty"` + DomainRole string `json:"domain_role,omitempty"` +} + +type RepositoryNode struct { + nodeHeader + Provider string `json:"provider,omitempty"` + Name string `json:"name,omitempty"` + Url string `json:"url"` + Owner string `json:"owner,omitempty"` + Description string `json:"description,omitempty"` + Stars int64 `json:"stars,omitempty"` + IsFork bool `json:"is_fork,omitempty"` + MatchedDorks []string `json:"matched_dorks,omitempty"` +} + +type SecretNode struct { + nodeHeader + Kind string `json:"kind,omitempty"` + Detector string `json:"detector,omitempty"` + Redacted string `json:"redacted,omitempty"` + Fingerprint string `json:"fingerprint"` + Source string `json:"source,omitempty"` + SourceUrl string `json:"source_url,omitempty"` + FilePath string `json:"file_path,omitempty"` + Line int64 `json:"line,omitempty"` + Commit string `json:"commit,omitempty"` + Verified bool `json:"verified,omitempty"` + Severity string `json:"severity,omitempty"` +} + +// SCONode is the common interface for all CSTX graph nodes. +type SCONode interface { + CstxType() string + CstxID() string +} + +type nodeHeader struct { + Type string `json:"cstx_type"` + ID string `json:"cstx_id"` +} + +func (h nodeHeader) CstxType() string { return h.Type } +func (h nodeHeader) CstxID() string { return h.ID } + +// ParseSCONode unmarshals a JSON node into the correct typed struct. +func ParseSCONode(data []byte) (SCONode, error) { + var h nodeHeader + if err := json.Unmarshal(data, &h); err != nil { + return nil, err + } + switch h.Type { + case "domain": + var v DomainNode + err := json.Unmarshal(data, &v) + return &v, err + case "subdomain": + var v SubdomainNode + err := json.Unmarshal(data, &v) + return &v, err + case "ip": + var v IpNode + err := json.Unmarshal(data, &v) + return &v, err + case "cidr": + var v CidrNode + err := json.Unmarshal(data, &v) + return &v, err + case "port": + var v PortNode + err := json.Unmarshal(data, &v) + return &v, err + case "app": + var v AppNode + err := json.Unmarshal(data, &v) + return &v, err + case "url": + var v UrlNode + err := json.Unmarshal(data, &v) + return &v, err + case "framework": + var v FrameworkNode + err := json.Unmarshal(data, &v) + return &v, err + case "vuln": + var v VulnNode + err := json.Unmarshal(data, &v) + return &v, err + case "sarif_vuln": + var v SarifVulnNode + err := json.Unmarshal(data, &v) + return &v, err + case "certificate": + var v CertificateNode + err := json.Unmarshal(data, &v) + return &v, err + case "company": + var v CompanyNode + err := json.Unmarshal(data, &v) + return &v, err + case "icp": + var v IcpNode + err := json.Unmarshal(data, &v) + return &v, err + case "bucket": + var v BucketNode + err := json.Unmarshal(data, &v) + return &v, err + case "endpoint": + var v EndpointNode + err := json.Unmarshal(data, &v) + return &v, err + case "host": + var v HostNode + err := json.Unmarshal(data, &v) + return &v, err + case "repository": + var v RepositoryNode + err := json.Unmarshal(data, &v) + return &v, err + case "secret": + var v SecretNode + err := json.Unmarshal(data, &v) + return &v, err + default: + return nil, nil + } +} diff --git a/go/sco_easm_test.go b/go/sco_easm_test.go new file mode 100644 index 0000000..e8d45ea --- /dev/null +++ b/go/sco_easm_test.go @@ -0,0 +1,153 @@ +package cstx + +import ( + "encoding/json" + "testing" +) + +func TestParseSCONode_Domain(t *testing.T) { + raw := `{"cstx_type":"domain","cstx_id":"domain:example.com","host":"example.com"}` + node, err := ParseSCONode([]byte(raw)) + if err != nil { + t.Fatal(err) + } + if node == nil { + t.Fatal("expected non-nil node") + } + if node.CstxType() != "domain" { + t.Errorf("expected type 'domain', got %q", node.CstxType()) + } + d := node.(*DomainNode) + if d.Host != "example.com" { + t.Errorf("expected host 'example.com', got %q", d.Host) + } +} + +func TestParseSCONode_Port(t *testing.T) { + raw := `{"cstx_type":"port","cstx_id":"port:1.2.3.4:80","ip":"1.2.3.4","port":"80","protocol":"tcp"}` + node, err := ParseSCONode([]byte(raw)) + if err != nil { + t.Fatal(err) + } + p := node.(*PortNode) + if p.Ip != "1.2.3.4" { + t.Errorf("ip: got %q", p.Ip) + } + if p.Port != "80" { + t.Errorf("port: got %q", p.Port) + } + if p.Protocol != "tcp" { + t.Errorf("protocol: got %q", p.Protocol) + } +} + +func TestParseSCONode_Ip(t *testing.T) { + raw := `{"cstx_type":"ip","cstx_id":"ip:10.0.0.1","ip":"10.0.0.1","country":"CN","cdn":true}` + node, err := ParseSCONode([]byte(raw)) + if err != nil { + t.Fatal(err) + } + ip := node.(*IpNode) + if ip.Ip != "10.0.0.1" { + t.Errorf("ip: got %q", ip.Ip) + } + if ip.Country != "CN" { + t.Errorf("country: got %q", ip.Country) + } + if !ip.Cdn { + t.Error("cdn should be true") + } +} + +func TestParseSCONode_Subdomain(t *testing.T) { + raw := `{"cstx_type":"subdomain","cstx_id":"subdomain:www.a.com","host":"www.a.com","a":["1.1.1.1","2.2.2.2"],"ttl":300}` + node, err := ParseSCONode([]byte(raw)) + if err != nil { + t.Fatal(err) + } + s := node.(*SubdomainNode) + if s.Host != "www.a.com" { + t.Errorf("host: got %q", s.Host) + } + if len(s.A) != 2 || s.A[0] != "1.1.1.1" { + t.Errorf("a records: got %v", s.A) + } + if s.Ttl != 300 { + t.Errorf("ttl: got %d", s.Ttl) + } +} + +func TestParseSCONode_UnknownType(t *testing.T) { + raw := `{"cstx_type":"unknown_type","cstx_id":"x"}` + node, err := ParseSCONode([]byte(raw)) + if err != nil { + t.Fatal(err) + } + if node != nil { + t.Error("unknown type should return nil node") + } +} + +func TestParseSCONode_AllTypes(t *testing.T) { + types := []string{ + "domain", "subdomain", "ip", "cidr", "port", "app", "url", + "framework", "vuln", "certificate", "company", "icp", + "bucket", "endpoint", "host", "repository", "secret", + } + for _, typ := range types { + raw, _ := json.Marshal(map[string]string{"cstx_type": typ, "cstx_id": typ + ":test"}) + node, err := ParseSCONode(raw) + if err != nil { + t.Errorf("type %q: parse error: %v", typ, err) + continue + } + if node == nil { + t.Errorf("type %q: got nil", typ) + continue + } + if node.CstxType() != typ { + t.Errorf("type %q: CstxType() = %q", typ, node.CstxType()) + } + } +} + +func TestRelationConstants(t *testing.T) { + expected := map[string]string{ + "RelResolve": RelResolve, + "RelOpen": RelOpen, + "RelHasSubdomain": RelHasSubdomain, + "RelContain": RelContain, + "RelHosts": RelHosts, + "RelUses": RelUses, + "RelRefers": RelRefers, + "RelSecuredBy": RelSecuredBy, + "RelExploit": RelExploit, + "RelAffect": RelAffect, + "RelInvest": RelInvest, + "RelOwn": RelOwn, + "RelFiledFor": RelFiledFor, + } + want := map[string]string{ + "RelResolve": "resolve", + "RelOpen": "open", + "RelHasSubdomain": "has-subdomain", + "RelContain": "contain", + "RelHosts": "hosts", + "RelUses": "uses", + "RelRefers": "refers", + "RelSecuredBy": "secured_by", + "RelExploit": "exploit", + "RelAffect": "affect", + "RelInvest": "invest", + "RelOwn": "own", + "RelFiledFor": "filed-for", + } + for name, got := range expected { + if got != want[name] { + t.Errorf("%s: want %q, got %q", name, want[name], got) + } + } + if len(RelationTypes) != len(want) { + t.Errorf("RelationTypes length: want %d, got %d", len(want), len(RelationTypes)) + } +} diff --git a/go/sco_gen.go b/go/sco_gen.go deleted file mode 100644 index 3284f91..0000000 --- a/go/sco_gen.go +++ /dev/null @@ -1,293 +0,0 @@ -// @generated by cstx-codegen — DO NOT EDIT. - -package cstx - -import "encoding/json" - -type Domain struct { - nodeHeader - Host string `json:"host"` -} - -type Subdomain struct { - nodeHeader - Host string `json:"host"` - IsTld bool `json:"is_tld,omitempty"` - Ttl int64 `json:"ttl,omitempty"` - Resolver []string `json:"resolver,omitempty"` - A []string `json:"a,omitempty"` - Aaaa []string `json:"aaaa,omitempty"` - Cname []string `json:"cname,omitempty"` - Mx []string `json:"mx,omitempty"` - Ns []string `json:"ns,omitempty"` - Txt []string `json:"txt,omitempty"` -} - -type Ip struct { - nodeHeader - Ip string `json:"ip"` - Country string `json:"country,omitempty"` - Area string `json:"area,omitempty"` - AsnNumber string `json:"asn_number,omitempty"` - AsName string `json:"as_name,omitempty"` - CdnName string `json:"cdn_name,omitempty"` - CloudName string `json:"cloud_name,omitempty"` - WafName string `json:"waf_name,omitempty"` - Cdn bool `json:"cdn,omitempty"` - Cloud bool `json:"cloud,omitempty"` - Waf bool `json:"waf,omitempty"` -} - -type Cidr struct { - nodeHeader - Cidr string `json:"cidr"` -} - -type Port struct { - nodeHeader - Ip string `json:"ip"` - Port string `json:"port"` - Protocol string `json:"protocol"` -} - -type App struct { - nodeHeader - AppId string `json:"app_id"` - Url string `json:"url,omitempty"` - Frameworks []string `json:"frameworks,omitempty"` - Title string `json:"title,omitempty"` - Midware string `json:"midware,omitempty"` - Status string `json:"status,omitempty"` - StatusCode int64 `json:"status_code,omitempty"` - Host string `json:"host,omitempty"` - ContentType string `json:"content_type,omitempty"` - BodyLength int64 `json:"body_length,omitempty"` - HeaderLength int64 `json:"header_length,omitempty"` -} - -type Url struct { - nodeHeader - Scheme string `json:"scheme"` - Host string `json:"host,omitempty"` - Port string `json:"port,omitempty"` - Path string `json:"path,omitempty"` - Ip string `json:"ip,omitempty"` - StatusCode int64 `json:"status_code,omitempty"` - Title string `json:"title,omitempty"` - BodyLength int64 `json:"body_length,omitempty"` - ContentType string `json:"content_type,omitempty"` - RedirectUrl string `json:"redirect_url,omitempty"` - Frameworks []string `json:"frameworks,omitempty"` -} - -type Framework struct { - nodeHeader - Name string `json:"name"` - Part string `json:"part,omitempty"` - Vendor string `json:"vendor,omitempty"` - Product string `json:"product,omitempty"` - Version string `json:"version,omitempty"` - Tags []string `json:"tags,omitempty"` - IsFocus bool `json:"is_focus,omitempty"` - Sources []string `json:"sources,omitempty"` -} - -type Vuln struct { - nodeHeader - Value string `json:"value"` - VulnId string `json:"vuln_id,omitempty"` - Name string `json:"name,omitempty"` - AssetId string `json:"asset_id,omitempty"` - Severity string `json:"severity,omitempty"` - Tags []string `json:"tags,omitempty"` - Ip string `json:"ip,omitempty"` - Host string `json:"host,omitempty"` - Port string `json:"port,omitempty"` - Protocol string `json:"protocol,omitempty"` - Scheme string `json:"scheme,omitempty"` - Url string `json:"url,omitempty"` - Path string `json:"path,omitempty"` - Pocname string `json:"pocname,omitempty"` - Request string `json:"request,omitempty"` - Response string `json:"response,omitempty"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Matched bool `json:"matched,omitempty"` - Extracted bool `json:"extracted,omitempty"` -} - -// SarifVuln is a SARIF v2.1.0 aligned vulnerability finding. -// Atomic fields are columnar in the graph; evidence holds detailed JSON (exchanges, suppression). -type SarifVuln struct { - nodeHeader - Value string `json:"value"` - VulnId string `json:"vuln_id,omitempty"` - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Source string `json:"source,omitempty"` - Target string `json:"target,omitempty"` - Tags []string `json:"tags,omitempty"` - AssetCstxId string `json:"asset_cstx_id,omitempty"` - Kind string `json:"kind,omitempty"` - Level string `json:"level,omitempty"` - BaselineState string `json:"baseline_state,omitempty"` - RuleId string `json:"rule_id,omitempty"` - Evidence string `json:"evidence,omitempty"` -} - -type Certificate struct { - nodeHeader - Fingerprint string `json:"fingerprint"` - Serial string `json:"serial,omitempty"` - Issuer string `json:"issuer,omitempty"` - Subject string `json:"subject,omitempty"` - NotBefore string `json:"not_before,omitempty"` - NotAfter string `json:"not_after,omitempty"` - San []string `json:"san,omitempty"` - Host string `json:"host,omitempty"` - Ip string `json:"ip,omitempty"` -} - -type Company struct { - nodeHeader - Name string `json:"name"` - Perc string `json:"perc,omitempty"` - Tycid string `json:"tycid,omitempty"` - Icp string `json:"icp,omitempty"` - Parent string `json:"parent,omitempty"` -} - -type Icp struct { - nodeHeader - Icp string `json:"icp"` - Sub string `json:"sub,omitempty"` - Date string `json:"date,omitempty"` - Company string `json:"company,omitempty"` - Title string `json:"title,omitempty"` - Domain string `json:"domain,omitempty"` - Ip string `json:"ip,omitempty"` -} - -type Bucket struct { - nodeHeader - Provider string `json:"provider,omitempty"` - Name string `json:"name,omitempty"` - Region string `json:"region,omitempty"` - Endpoint string `json:"endpoint"` - Acl string `json:"acl,omitempty"` - ObjectCount int64 `json:"object_count,omitempty"` - KnownPaths []string `json:"known_paths,omitempty"` - SourceUrl string `json:"source_url,omitempty"` -} - -type Endpoint struct { - nodeHeader - Url string `json:"url"` - Method string `json:"method,omitempty"` - Path string `json:"path,omitempty"` - ContentType string `json:"content_type,omitempty"` - StatusCode int64 `json:"status_code,omitempty"` - Source string `json:"source,omitempty"` - SourceUrl string `json:"source_url,omitempty"` - Parameters []string `json:"parameters,omitempty"` - Tags []string `json:"tags,omitempty"` -} - -type Host struct { - nodeHeader - Hostname string `json:"hostname"` - LocalIps []string `json:"local_ips,omitempty"` - GatewayIps []string `json:"gateway_ips,omitempty"` - DnsServers []string `json:"dns_servers,omitempty"` - DomainName string `json:"domain_name,omitempty"` - DomainRole string `json:"domain_role,omitempty"` -} - -type Repository struct { - nodeHeader - Provider string `json:"provider,omitempty"` - Name string `json:"name,omitempty"` - Url string `json:"url"` - Owner string `json:"owner,omitempty"` - Description string `json:"description,omitempty"` - Stars int64 `json:"stars,omitempty"` - IsFork bool `json:"is_fork,omitempty"` - MatchedDorks []string `json:"matched_dorks,omitempty"` -} - -type Secret struct { - nodeHeader - Kind string `json:"kind,omitempty"` - Detector string `json:"detector,omitempty"` - Redacted string `json:"redacted,omitempty"` - Fingerprint string `json:"fingerprint"` - Source string `json:"source,omitempty"` - SourceUrl string `json:"source_url,omitempty"` - FilePath string `json:"file_path,omitempty"` - Line int64 `json:"line,omitempty"` - Commit string `json:"commit,omitempty"` - Verified bool `json:"verified,omitempty"` - Severity string `json:"severity,omitempty"` -} - -// SCONode is the common interface for all CSTX graph nodes. -type SCONode interface { - CstxType() string - CstxID() string -} - -type nodeHeader struct { - Type string `json:"cstx_type"` - ID string `json:"cstx_id"` -} - -func (h nodeHeader) CstxType() string { return h.Type } -func (h nodeHeader) CstxID() string { return h.ID } - -// ParseSCONode unmarshals a JSON node into the correct typed struct. -func ParseSCONode(data []byte) (SCONode, error) { - var h nodeHeader - if err := json.Unmarshal(data, &h); err != nil { - return nil, err - } - switch h.Type { - case "domain": - var v Domain; err := json.Unmarshal(data, &v); return &v, err - case "subdomain": - var v Subdomain; err := json.Unmarshal(data, &v); return &v, err - case "ip": - var v Ip; err := json.Unmarshal(data, &v); return &v, err - case "cidr": - var v Cidr; err := json.Unmarshal(data, &v); return &v, err - case "port": - var v Port; err := json.Unmarshal(data, &v); return &v, err - case "app": - var v App; err := json.Unmarshal(data, &v); return &v, err - case "url": - var v Url; err := json.Unmarshal(data, &v); return &v, err - case "framework": - var v Framework; err := json.Unmarshal(data, &v); return &v, err - case "vuln": - var v Vuln; err := json.Unmarshal(data, &v); return &v, err - case "sarif_vuln": - var v SarifVuln; err := json.Unmarshal(data, &v); return &v, err - case "certificate": - var v Certificate; err := json.Unmarshal(data, &v); return &v, err - case "company": - var v Company; err := json.Unmarshal(data, &v); return &v, err - case "icp": - var v Icp; err := json.Unmarshal(data, &v); return &v, err - case "bucket": - var v Bucket; err := json.Unmarshal(data, &v); return &v, err - case "endpoint": - var v Endpoint; err := json.Unmarshal(data, &v); return &v, err - case "host": - var v Host; err := json.Unmarshal(data, &v); return &v, err - case "repository": - var v Repository; err := json.Unmarshal(data, &v); return &v, err - case "secret": - var v Secret; err := json.Unmarshal(data, &v); return &v, err - default: - return nil, nil - } -} diff --git a/go/scripts/download.sh b/go/scripts/download.sh deleted file mode 100755 index 32f073e..0000000 --- a/go/scripts/download.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -set -e - -REPO="chainreactors/libcstx" -VERSION="${CSTX_VERSION:-latest}" - -detect_platform() { - OS=$(uname -s | tr '[:upper:]' '[:lower:]') - ARCH=$(uname -m) - case "$OS" in - linux) OS="linux" ;; - darwin) OS="darwin" ;; - mingw*|msys*|cygwin*) OS="windows" ;; - *) echo "unsupported OS: $OS" >&2; exit 1 ;; - esac - case "$ARCH" in - x86_64|amd64) ARCH="amd64" ;; - aarch64|arm64) ARCH="arm64" ;; - *) echo "unsupported arch: $ARCH" >&2; exit 1 ;; - esac - echo "${OS}_${ARCH}" -} - -PLATFORM=$(detect_platform) -LIB_DIR="$(cd "$(dirname "$0")/.." && pwd)/lib/${PLATFORM}" -MARKER="${LIB_DIR}/.version" - -if [ "$VERSION" = "latest" ]; then - VERSION=$(curl -sL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') - if [ -z "$VERSION" ]; then - echo "failed to fetch latest release version" >&2 - exit 1 - fi -fi - -if [ -f "$MARKER" ] && [ "$(cat "$MARKER")" = "$VERSION" ]; then - echo "libcstx_ffi ${VERSION} already installed for ${PLATFORM}" - exit 0 -fi - -echo "downloading libcstx_ffi ${VERSION} for ${PLATFORM}..." -URL="https://github.com/${REPO}/releases/download/${VERSION}/libcstx_ffi-${PLATFORM}.tar.gz" - -mkdir -p "$LIB_DIR" -curl -sL "$URL" | tar -xz -C "$LIB_DIR" -echo "$VERSION" > "$MARKER" -echo "installed libcstx_ffi ${VERSION} → ${LIB_DIR}" diff --git a/go/sro_easm.go b/go/sro_easm.go new file mode 100644 index 0000000..b2590bc --- /dev/null +++ b/go/sro_easm.go @@ -0,0 +1,35 @@ +// @generated by cstx-codegen — DO NOT EDIT. + +package cstx + +const ( + RelResolve = "resolve" + RelOpen = "open" + RelHasSubdomain = "has-subdomain" + RelContain = "contain" + RelHosts = "hosts" + RelUses = "uses" + RelRefers = "refers" + RelSecuredBy = "secured_by" + RelExploit = "exploit" + RelAffect = "affect" + RelInvest = "invest" + RelOwn = "own" + RelFiledFor = "filed-for" +) + +var RelationTypes = []string{ + RelResolve, + RelOpen, + RelHasSubdomain, + RelContain, + RelHosts, + RelUses, + RelRefers, + RelSecuredBy, + RelExploit, + RelAffect, + RelInvest, + RelOwn, + RelFiledFor, +} diff --git a/go/sro_gen.go b/go/sro_gen.go deleted file mode 100644 index a39c1a5..0000000 --- a/go/sro_gen.go +++ /dev/null @@ -1,21 +0,0 @@ -// @generated by cstx-codegen — DO NOT EDIT. - -package cstx - -const ( - RelResolve = "resolve" - RelOpen = "open" - RelSecuredBy = "secured_by" - RelInvest = "invest" - RelOwn = "own" - RelFiledFor = "filed-for" -) - -var RelationTypes = []string{ - RelResolve, - RelOpen, - RelSecuredBy, - RelInvest, - RelOwn, - RelFiledFor, -} diff --git a/go/transform.go b/go/transform.go deleted file mode 100644 index d2ca4e8..0000000 --- a/go/transform.go +++ /dev/null @@ -1,67 +0,0 @@ -package cstx - -import ( - "bytes" - "encoding/json" - "fmt" - "reflect" -) - -// Parse converts tool output into typed SCO nodes (stateless transform). -// tool: artifact name ("gogo", "spray", "zombie", "neutron", etc.) -// input: []byte (raw JSONL), a struct, or a slice of structs. -func Parse(tool string, input any) ([]SCONode, error) { - data, err := toJSONL(input) - if err != nil { - return nil, err - } - raw, err := Transform(tool, data) - if err != nil { - return nil, err - } - return parseSCONodes(raw) -} - -func parseSCONodes(data []byte) ([]SCONode, error) { - var rawNodes []json.RawMessage - if err := json.Unmarshal(data, &rawNodes); err != nil { - return nil, fmt.Errorf("cstx parse: %w", err) - } - nodes := make([]SCONode, 0, len(rawNodes)) - for _, r := range rawNodes { - n, err := ParseSCONode(r) - if err != nil { - continue - } - if n != nil { - nodes = append(nodes, n) - } - } - return nodes, nil -} - -func toJSONL(input any) ([]byte, error) { - if b, ok := input.([]byte); ok { - return b, nil - } - rv := reflect.ValueOf(input) - if rv.Kind() == reflect.Ptr { - rv = rv.Elem() - } - if rv.Kind() != reflect.Slice { - b, err := json.Marshal(input) - if err != nil { - return nil, err - } - return append(b, '\n'), nil - } - var buf bytes.Buffer - enc := json.NewEncoder(&buf) - enc.SetEscapeHTML(false) - for i := 0; i < rv.Len(); i++ { - if err := enc.Encode(rv.Index(i).Interface()); err != nil { - return nil, fmt.Errorf("cstx marshal [%d]: %w", i, err) - } - } - return buf.Bytes(), nil -} diff --git a/go/types.go b/go/types.go index a315fe3..d4f215d 100644 --- a/go/types.go +++ b/go/types.go @@ -1,16 +1,140 @@ package cstx -type BatchResult struct { - NewNodes int `json:"new_nodes"` - UpdatedNodes int `json:"updated_nodes"` - Count int `json:"count"` -} - -type IngestResult struct { - RecordsParsed int `json:"records_parsed"` - NewNodes int `json:"new_nodes"` - UpdatedNodes int `json:"updated_nodes"` - NewEdges int `json:"new_edges"` - NodeCount int `json:"node_count"` - EdgeCount int `json:"edge_count"` +import "encoding/json" + +// NodeFlags are engine-compatible bit constants. Graph APIs accept ordinary +// uint64 masks built from these values. +const ( + FlagNone uint64 = 0 + FlagHoneypot uint64 = 1 << 0 + FlagNoise uint64 = 1 << 1 + FlagFalsePositive uint64 = 1 << 2 + FlagManualIgnored uint64 = 1 << 3 + FlagThreatPresent uint64 = 1 << 4 + FlagHistoricVulnerable uint64 = 1 << 5 + FlagInternal uint64 = 1 << 6 +) + +// FlagsAllMask contains every currently defined node flag. +const FlagsAllMask uint64 = FlagHoneypot | FlagNoise | FlagFalsePositive | + FlagManualIgnored | FlagThreatPresent | FlagHistoricVulnerable | FlagInternal + +// FlagsDefaultExcludeMask is the engine's standard default-exclusion mask. +const FlagsDefaultExcludeMask uint64 = FlagHoneypot | FlagNoise | FlagFalsePositive | FlagManualIgnored + +// Order is the deterministic ordering applied to a collection cursor. +type Order string + +const ( + OrderUnspecified Order = "unspecified" + OrderIDAsc Order = "id_asc" + OrderIDDesc Order = "id_desc" +) + +// Node is the canonical graph node exchanged with the Rust runtime. Model +// holds schema-typed fields plus the reserved keys "__node_type__", +// "cstx_flags", "created_at", and "updated_at". +type Node struct { + ID string `json:"id"` + Type string `json:"type"` + Value any `json:"value"` + Model map[string]any `json:"model"` + Sources []string `json:"sources"` + Extras map[string]any `json:"extras"` +} + +// MarshalJSON keeps list fields as [] rather than null; the Rust contract +// requires every field to be present. +func (n Node) MarshalJSON() ([]byte, error) { + type wire Node + if n.Sources == nil { + n.Sources = []string{} + } + if n.Model == nil { + n.Model = map[string]any{} + } + if n.Extras == nil { + n.Extras = map[string]any{} + } + return json.Marshal(wire(n)) +} + +// Edge is the canonical graph relationship exchanged with the Rust runtime. +type Edge struct { + ID string `json:"id"` + SourceID string `json:"source_id"` + TargetID string `json:"target_id"` + RelationType string `json:"relation_type"` + Sources []string `json:"sources"` + Attrs map[string]any `json:"attrs"` +} + +// MarshalJSON keeps list fields as [] rather than null. +func (e Edge) MarshalJSON() ([]byte, error) { + type wire Edge + if e.Sources == nil { + e.Sources = []string{} + } + if e.Attrs == nil { + e.Attrs = map[string]any{} + } + return json.Marshal(wire(e)) +} + +// GraphStats is a small aggregate count summary. +type GraphStats struct { + Nodes map[string]int64 `json:"nodes"` + Edges map[string]int64 `json:"edges"` + Sources map[string]int64 `json:"sources"` +} + +// ChangeSet lists the IDs changed by one successfully committed mutation. +type ChangeSet struct { + AddedNodeIDs []string `json:"added_node_ids"` + UpdatedNodeIDs []string `json:"updated_node_ids"` + RemovedNodeIDs []string `json:"removed_node_ids"` + AddedEdgeIDs []string `json:"added_edge_ids"` + UpdatedEdgeIDs []string `json:"updated_edge_ids"` + RemovedEdgeIDs []string `json:"removed_edge_ids"` + Reset bool `json:"reset"` +} + +// Affected returns the total number of changed graph elements. +func (c ChangeSet) Affected() int { + return len(c.AddedNodeIDs) + len(c.UpdatedNodeIDs) + len(c.RemovedNodeIDs) + + len(c.AddedEdgeIDs) + len(c.UpdatedEdgeIDs) + len(c.RemovedEdgeIDs) +} + +// Commit describes one immutable graph tree on a repository ref. +type Commit struct { + ID string `json:"id"` + Tree string `json:"tree"` + Parent string `json:"parent"` + Message string `json:"message"` + Metadata any `json:"metadata"` + Stats any `json:"stats"` + CreatedAt int64 `json:"created_at"` +} + +// GraphDiff groups added, removed, and modified element IDs by element type. +type GraphDiff struct { + Added map[string][]string `json:"added"` + Removed map[string][]string `json:"removed"` + Modified map[string][]string `json:"modified"` +} + +// Ref is one named repository reference and its commit ID. +type Ref struct { + Name string + Head string +} + +// UnmarshalJSON decodes the Rust (name, head) tuple transport. +func (r *Ref) UnmarshalJSON(data []byte) error { + var pair [2]string + if err := json.Unmarshal(data, &pair); err != nil { + return err + } + r.Name, r.Head = pair[0], pair[1] + return nil } diff --git a/include/cstx_ffi.h b/include/cstx_ffi.h index 60f6e64..bf3225d 100644 --- a/include/cstx_ffi.h +++ b/include/cstx_ffi.h @@ -8,282 +8,178 @@ #include #include -typedef struct CstxCasStore CstxCasStore; - -typedef struct CstxGraph CstxGraph; - -struct CstxGraph *cstx_graph_new(void); - -void cstx_graph_free(struct CstxGraph *g); - -int cstx_graph_load_plugin(struct CstxGraph *g, const char *name); - -int cstx_graph_load_all_plugins(struct CstxGraph *g); - -char *cstx_available_plugins(void); - -int cstx_graph_register_schema(struct CstxGraph *g, - const char *node_type, - const char *json_schema_str, - const char *value_field); - -int cstx_graph_add_join_rule(struct CstxGraph *g, - const char *left_type, - const char *right_type, - const char *relation, - const char *left_key, - const char *right_key, - int predicted, - const char *left_target_id, - const char *right_source_id); - -int cstx_graph_add_node(struct CstxGraph *g, - const char *node_type, - const char *cstx_id, - const char *payload_json, - const char *sources_json, - uint64_t cstx_flags); - -int cstx_graph_add_edge(struct CstxGraph *g, - const char *source_id, - const char *target_id, - const char *relation, - const char *data_source); - -int cstx_graph_add_nodes_batch(struct CstxGraph *g, - const char *nodes_json, - char **out, - uintptr_t *out_len); - -int cstx_graph_add_edges_batch(struct CstxGraph *g, const char *edges_json); - -int cstx_graph_link(struct CstxGraph *g, - const char *source, - const uint8_t *data, - uintptr_t data_len, - char **out, - uintptr_t *out_len); - -int cstx_graph_ingest_native(struct CstxGraph *g, - const char *plugin_name, - const char *artifact, - const uint8_t *data, - uintptr_t data_len, - char **out, - uintptr_t *out_len); - -int cstx_graph_has_native_artifact(struct CstxGraph *g, const char *artifact); - -int cstx_graph_ingest_jsonl(struct CstxGraph *g, - const char *node_type, - const uint8_t *data, - uintptr_t data_len, - const char *id_expr, - const char *data_source, - char **out, - uintptr_t *out_len); - -int cstx_graph_link_nodes(struct CstxGraph *g, - const char *data_source, - char **out, - uintptr_t *out_len); - -char *cstx_graph_node(struct CstxGraph *g, const char *node_id); - -char *cstx_graph_nodes(struct CstxGraph *g, const char *type_filter); - -char *cstx_graph_edges(struct CstxGraph *g, const char *relation); - -char *cstx_graph_node_types(struct CstxGraph *g); - -char *cstx_graph_neighbors(struct CstxGraph *g, const char *node_id); - -char *cstx_graph_to_json(struct CstxGraph *g); - -char *cstx_graph_node_payload(struct CstxGraph *g, const char *node_id); - -int cstx_graph_contains_node(struct CstxGraph *g, const char *node_id); - -uintptr_t cstx_graph_node_count(struct CstxGraph *g); - -uintptr_t cstx_graph_edge_count(struct CstxGraph *g); - -char *cstx_graph_node_ids(struct CstxGraph *g); - -char *cstx_graph_stats(struct CstxGraph *g); - -char *cstx_graph_all_nodes_json(struct CstxGraph *g); - -char *cstx_graph_neighbor_ids(struct CstxGraph *g, const char *node_id, const char *direction); - -char *cstx_graph_bfs(struct CstxGraph *g, const char *seed_id, uint32_t depth, int reverse); - -char *cstx_graph_shortest_paths(struct CstxGraph *g, - const char *start_id, - const char *end_id, - uint32_t max_depth); - -uintptr_t cstx_graph_degree(struct CstxGraph *g, const char *node_id, const char *direction); - -int cstx_graph_subgraph_node_ids(struct CstxGraph *g, - const char *seed_ids_json, - uint32_t depth, - char **out, - uintptr_t *out_len); - -int cstx_graph_query_dsl(struct CstxGraph *g, - const char *expression, - intptr_t limit, - uintptr_t offset, - char **out, - uintptr_t *out_len); - -char *cstx_graph_query_node_ids(struct CstxGraph *g, - const char *expression, - intptr_t limit, - uintptr_t offset); - -int cstx_graph_is_path_expression(struct CstxGraph *g, const char *expression); - -char *cstx_graph_edges_filtered(struct CstxGraph *g, - const char *source_id, - const char *target_id, - const char *relation); - -char *cstx_graph_export_snapshot_json(struct CstxGraph *g); - -int cstx_transform(const char *source_type, - const uint8_t *data, - uintptr_t data_len, - char **out, - uintptr_t *out_len); - -void cstx_free_string(char *s); - -const char *cstx_version(void); - -char *cstx_supported_artifacts(void); - -uint64_t cstx_flags_all_mask(void); - -uint64_t cstx_flags_default_exclude_mask(void); - -int cstx_graph_update_node_flags(struct CstxGraph *g, - const char *node_id, - uint64_t add, - uint64_t remove, - int64_t set_to); - -struct CstxCasStore *cstx_cas_store_new(void); - -void cstx_cas_store_free(struct CstxCasStore *s); - -/** - * Load a root tree object into the store. - * kind: "root" | "map" — determines how to parse the JSON. - * For "root": JSON is {"node_types":{"type":"hash",...},"edge_types":{...}} - * For "map": JSON is {"key":"hash",...} - */ -int cstx_cas_store_load_root(struct CstxCasStore *s, const char *hash, const char *json_str); - -/** - * Load a map (type-tree or bucket) object into the store. - */ -int cstx_cas_store_load_map(struct CstxCasStore *s, const char *hash, const char *json_str); - -/** - * Export all tree objects from the store as JSON. - * Returns: {"hash": {"key": "value", ...}, ...} - */ -char *cstx_cas_store_export(struct CstxCasStore *s, const char *root_hash); - -/** - * SHA-256 of canonical JSON. Caller frees result. - */ -char *cstx_cas_canonical_hash(const char *json_str); - -/** - * Build a Merkle tree from entries JSON. - * entries_json: {"type": {"id": "hash", ...}, ...} - * Returns root hash. Caller frees. - */ -char *cstx_cas_tree_build(struct CstxCasStore *s, const char *entries_json); - -/** - * Update a Merkle tree incrementally. - * changes_json: {"type": {"id": "new_hash_or_null", ...}, ...} - * null values = delete. Returns new root hash. Caller frees. - */ -char *cstx_cas_tree_update(struct CstxCasStore *s, const char *root_hash, const char *changes_json); - -/** - * Diff two Merkle trees. Returns JSON: {"added":{},"removed":{},"modified":{}} - * Caller frees. - */ -char *cstx_cas_tree_diff(struct CstxCasStore *s, const char *base_hash, const char *head_hash); - -/** - * Find an element in the tree by ID (searches all types). - * Returns content hash or NULL if not found. Caller frees. - */ -char *cstx_cas_tree_find(struct CstxCasStore *s, const char *root_hash, const char *element_id); - -/** - * Materialize all entries from a tree. - * Returns JSON: {"type": {"id": "hash", ...}, ...}. Caller frees. - */ -char *cstx_cas_tree_entries(struct CstxCasStore *s, const char *root_hash); - -int cstx_graph_semantic_texts(struct CstxGraph *g, - const char *node_types_json, - char **out, - uintptr_t *out_len); - -void cstx_graph_init_rag(struct CstxGraph *g, uintptr_t dims); - -/** - * entries_json: [[arena_idx, "node_type", [f32, ...]], ...] - */ -int cstx_graph_set_vectors(struct CstxGraph *g, - const char *entries_json, - char **out, - uintptr_t *out_len); - -int cstx_graph_build_indices(struct CstxGraph *g, char **out, uintptr_t *out_len); - -/** - * Returns JSON: [[arena_idx, score, "cstx_id"], ...] - */ -int cstx_graph_rag_search(struct CstxGraph *g, - const char *query_text, - const char *query_vector_json, - const char *strategy, - const char *node_types_json, - uintptr_t top_k, - char **out, - uintptr_t *out_len); - -/** - * Returns JSON: {"count": N, "modularity": f64} - */ -int cstx_graph_detect_communities(struct CstxGraph *g, - double resolution, - uintptr_t min_community_size, - char **out, - uintptr_t *out_len); - -int cstx_graph_set_community_summary(struct CstxGraph *g, - uint32_t community_id, - const char *summary, - char **out, - uintptr_t *out_len); +enum CstxStatusCode +#if __STDC_VERSION__ >= 202311L + : int32_t +#endif // __STDC_VERSION__ >= 202311L + { + Ok = 0, + InvalidArgument = 1, + NotFound = 2, + Conflict = 3, + NotInitialized = 4, + Unsupported = 5, + Internal = 6, + Validation = 7, + Parse = 8, + Io = 9, + CorruptData = 10, + CursorInvalidated = 11, +}; +#if __STDC_VERSION__ >= 202311L +typedef enum CstxStatusCode CstxStatusCode; +#else +typedef int32_t CstxStatusCode; +#endif // __STDC_VERSION__ >= 202311L + +typedef struct CstxEdgeIterator CstxEdgeIterator; + +typedef struct CstxHandle CstxHandle; + +typedef struct CstxNodeIterator CstxNodeIterator; + +typedef struct CstxBuffer { + uint8_t *data; + uintptr_t len; +} CstxBuffer; + +typedef struct CstxSlice { + const uint8_t *data; + uintptr_t len; +} CstxSlice; /** - * Returns JSON array of arena indices: [u32, ...] + * Release a Rust-owned output or error buffer. */ -int cstx_graph_community_members(struct CstxGraph *g, - uint32_t community_id, - char **out, - uintptr_t *out_len); +void cstx_buffer_free(struct CstxBuffer *buffer); + +CstxStatusCode cstx_open(struct CstxSlice config_json, + struct CstxHandle **output, + struct CstxBuffer *error); + +void cstx_close(struct CstxHandle *handle); + +void cstx_free(struct CstxHandle *handle); + +CstxStatusCode cstx_last_change_json(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_register(struct CstxHandle *handle, + struct CstxSlice node_type, + struct CstxSlice schema_json, + struct CstxSlice value_field, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_contains(struct CstxHandle *handle, + struct CstxSlice node_type, + uint8_t *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_get_json(struct CstxHandle *handle, + struct CstxSlice node_type, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_load_plugin(struct CstxHandle *handle, + struct CstxSlice name, + struct CstxBuffer *error); + +CstxStatusCode cstx_schema_load_all_plugins(struct CstxHandle *handle, struct CstxBuffer *error); + +CstxStatusCode cstx_schema_available_plugins_json(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_ingest(struct CstxHandle *handle, + struct CstxSlice source, + struct CstxSlice data, + uint64_t *affected, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_node(struct CstxHandle *handle, + struct CstxSlice node_id, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_contains(struct CstxHandle *handle, + struct CstxSlice node_id, + uint8_t *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_stats(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_nodes(struct CstxHandle *handle, + struct CstxSlice filter_json, + struct CstxSlice options_json, + struct CstxNodeIterator **output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_edges(struct CstxHandle *handle, + struct CstxSlice filter_json, + struct CstxSlice options_json, + struct CstxEdgeIterator **output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_neighbors(struct CstxHandle *handle, + struct CstxSlice node_id, + struct CstxSlice direction, + struct CstxSlice options_json, + struct CstxNodeIterator **output, + struct CstxBuffer *error); + +CstxStatusCode cstx_graph_query(struct CstxHandle *handle, + struct CstxSlice expression, + struct CstxSlice options_json, + struct CstxNodeIterator **output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_commit(struct CstxHandle *handle, + struct CstxSlice ref_name, + struct CstxSlice message, + struct CstxSlice metadata_json, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_diff(struct CstxHandle *handle, + struct CstxSlice base_ref, + struct CstxSlice head_ref, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_dump(struct CstxHandle *handle, + struct CstxSlice compression, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_load(struct CstxHandle *handle, + struct CstxSlice data, + struct CstxSlice compression, + uint64_t *consumed, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_dump_json(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_load_json(struct CstxHandle *handle, + struct CstxSlice data, + uint64_t *consumed, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_snapshot_fingerprint(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_head(struct CstxHandle *handle, + struct CstxSlice ref_name, + struct CstxBuffer *output, + struct CstxBuffer *error); + +CstxStatusCode cstx_repo_refs(struct CstxHandle *handle, + struct CstxBuffer *output, + struct CstxBuffer *error); #endif /* CSTX_FFI_H */ diff --git a/python/Cargo.lock b/python/Cargo.lock deleted file mode 100644 index 11b19c2..0000000 --- a/python/Cargo.lock +++ /dev/null @@ -1,861 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "anyhow" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "bitflags" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cstx-core-py" -version = "0.2.0" -dependencies = [ - "cstx-easm", - "cstx-graph", - "pyo3", - "serde_json", - "smallvec", -] - -[[package]] -name = "cstx-derive" -version = "0.2.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cstx-easm" -version = "0.2.0" -dependencies = [ - "ahash", - "anyhow", - "cstx-graph", - "idna", - "inventory", - "serde", - "serde_json", - "smallvec", -] - -[[package]] -name = "cstx-graph" -version = "0.2.0" -dependencies = [ - "ahash", - "anyhow", - "cstx-derive", - "inventory", - "itoa", - "parking_lot", - "petgraph", - "rayon", - "serde", - "serde_json", - "sha1", - "smallvec", - "thiserror", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "inventory" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" -dependencies = [ - "rustversion", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pyo3" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sha1" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -dependencies = [ - "serde", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/python/Cargo.toml b/python/Cargo.toml index e1cf4f3..1bdbc29 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,19 +1,16 @@ [package] name = "cstxpy" -version = "0.2.0" -edition = "2021" +version.workspace = true +edition.workspace = true +workspace = "../../cstx-core" [lib] name = "_cstxpy" crate-type = ["cdylib"] [dependencies] -cstx-graph = { path = "../../cstx-core/crates/cstx-graph" } -cstx-easm = { path = "../../cstx-core/crates/cstx-easm" } -pyo3 = { version = "0.22", features = ["extension-module"] } -serde_json = "1" -smallvec = "1.13" +cstx-core = { path = "../../cstx-core/crates/cstx-core", default-features = false, features = ["python"] } +pyo3 = { workspace = true, features = ["extension-module", "abi3-py310", "generate-import-lib"] } -[features] -default = [] -abi3 = ["pyo3/abi3-py310"] +[lints] +workspace = true diff --git a/python/cstxpy/__init__.py b/python/cstxpy/__init__.py deleted file mode 100644 index f9a9e7e..0000000 --- a/python/cstxpy/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -try: - from cstxpy._cstxpy import Graph -except ImportError: - Graph = None # type: ignore[assignment,misc] - -from cstxpy.sco_gen import ( - SCOBase, - Domain, - Subdomain, - Ip, - Cidr, - Port, - App, - Url, - Framework, - Vuln, - Certificate, - Company, - Icp, - Bucket, - Endpoint, - Host, - Repository, - Secret, - SCONode, - SCO_TYPE_MAP, - parse_sco_node, -) - -from cstxpy.sro_gen import ( - RelationType, - RELATION_TYPES, - SROBase, -) - -__all__ = [ - "Graph", - # SCO types - "SCOBase", - "Domain", - "Subdomain", - "Ip", - "Cidr", - "Port", - "App", - "Url", - "Framework", - "Vuln", - "Certificate", - "Company", - "Icp", - "Bucket", - "Endpoint", - "Host", - "Repository", - "Secret", - "SCONode", - "SCO_TYPE_MAP", - "parse_sco_node", - # SRO types - "RelationType", - "RELATION_TYPES", - "SROBase", -] diff --git a/python/cstxpy/sro_gen.py b/python/cstxpy/sro_gen.py deleted file mode 100644 index 629a0ec..0000000 --- a/python/cstxpy/sro_gen.py +++ /dev/null @@ -1,28 +0,0 @@ -# @generated by cstx-codegen — DO NOT EDIT MANUALLY - -from typing import List, Literal - -from pydantic import BaseModel, ConfigDict - - -RelationType = Literal["resolve", "open", "secured_by", "invest", "own", "filed-for"] - -RELATION_TYPES: List[str] = ["resolve", "open", "secured_by", "invest", "own", "filed-for"] - - -class SROBase(BaseModel): - """Base model for all CSTX SRO (relationship) types.""" - - model_config = ConfigDict(extra="allow") - - source_id: str - target_id: str - relation_type: RelationType - sources: List[str] - - -__all__ = [ - "RelationType", - "RELATION_TYPES", - "SROBase", -] diff --git a/python/pyproject.toml b/python/pyproject.toml index 6c3501b..de3df52 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,19 +1,26 @@ [build-system] -requires = ["maturin>=1.5,<2.0"] +requires = ["maturin>=1.7,<2.0"] build-backend = "maturin" [project] name = "cstxpy" -version = "0.2.0" -description = "CSTX Graph Engine — native Rust bindings for Python" requires-python = ">=3.10" -license = "AGPL-3.0" +description = "Native Python bindings for the CSTX unified runtime" +dynamic = ["version"] +dependencies = ["pydantic>=2.0.0,<3.0.0"] +license = { text = "MIT" } classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] [tool.maturin] -features = ["pyo3/extension-module", "abi3"] +manifest-path = "Cargo.toml" module-name = "cstxpy._cstxpy" -python-source = "." +python-source = "python" diff --git a/python/python/cstxpy/__init__.py b/python/python/cstxpy/__init__.py new file mode 100644 index 0000000..68e43b6 --- /dev/null +++ b/python/python/cstxpy/__init__.py @@ -0,0 +1,35 @@ +"""Low-level CSTX v0.3 runtime implemented by the shared Rust core.""" + +from cstxpy._cstxpy import ( + CSTX, + CSTXError, + EdgeCursor, + Graph, + Rag, + RagRetrieval, + RagIndexSession, + RagRecordCursor, + NodeCursor, + NodeFlags, + Repository, + Schemas, + __version__, + is_path_expression, +) + +__all__ = [ + "CSTX", + "CSTXError", + "Schemas", + "Graph", + "Rag", + "RagRetrieval", + "RagIndexSession", + "RagRecordCursor", + "Repository", + "NodeCursor", + "EdgeCursor", + "NodeFlags", + "is_path_expression", + "__version__", +] diff --git a/python/python/cstxpy/_cstxpy.pyi b/python/python/cstxpy/_cstxpy.pyi new file mode 100644 index 0000000..a98c813 --- /dev/null +++ b/python/python/cstxpy/_cstxpy.pyi @@ -0,0 +1,684 @@ +"""Typed public surface for the low-level CSTX v0.3 Rust runtime. + +The binding deliberately exposes ordinary ``dict``, ``list``, ``bytes``, +``int``, keyword arguments, and iterators. It does not introduce public node, +filter, or options wrapper classes. ``*_json`` methods are explicit transport +fast paths for data that is already JSON or must leave CSTX as JSON; they are +not replacements for the native Python APIs. +""" + +from __future__ import annotations + +from typing import Any, Iterator + +__version__: str + + +def is_path_expression(expression: str) -> bool: + """Classify CSTX path syntax with the native parser.""" + ... + + +class CSTXError(Exception): + """Stable CSTX error whose attributes can be handled without parsing text.""" + + code: str + """Machine-readable error category such as ``NOT_FOUND``.""" + operation: str + """Public operation that failed, for example ``graph.add_nodes``.""" + item_index: int | None + """Index of the failing batch item when the error is item-specific.""" + field: str | None + """Canonical field associated with the failure when known.""" + expected: str | None + """Expected type or value description when available.""" + actual: str | None + """Observed type or value description when available.""" + + +class NodeCursor(Iterator[dict[str, Any]]): + """Lazy node dictionaries that avoid materializing a full Python result set.""" + + @property + def closed(self) -> bool: + """Whether the cursor was explicitly closed.""" + ... + + def close(self) -> None: + """Release cursor state; repeated calls are safe.""" + ... + + def __enter__(self) -> NodeCursor: + """Use the cursor as a context manager for deterministic cleanup.""" + ... + + def __exit__(self, *args: Any) -> None: + """Close the cursor when its context exits.""" + ... + + +class EdgeCursor(Iterator[dict[str, Any]]): + """Lazy edge dictionaries that postpone Python object construction.""" + + @property + def closed(self) -> bool: + """Whether the cursor was explicitly closed.""" + ... + + def close(self) -> None: + """Release cursor state; repeated calls are safe.""" + ... + + def __enter__(self) -> EdgeCursor: + """Use the cursor as a context manager for deterministic cleanup.""" + ... + + def __exit__(self, *args: Any) -> None: + """Close the cursor when its context exits.""" + ... + + +class Schemas: + """Schema/plugin namespace sharing state with its owning ``CSTX`` runtime.""" + + def register( + self, + node_type: str, + schema: dict[str, Any], + value_field: str | None = None, + ) -> None: + """Register canonical validation metadata without a Python schema wrapper.""" + ... + + def contains(self, node_type: str) -> bool: + """Check schema existence without materializing the schema dictionary.""" + ... + + def get(self, node_type: str) -> dict[str, Any]: + """Return one retained schema as an ordinary dictionary.""" + ... + + def list(self) -> list[dict[str, Any]]: + """Return retained schemas in deterministic node-type order.""" + ... + + def load_plugin(self, name: str) -> None: + """Load one linked native plugin into the shared graph engine.""" + ... + + def load_all_plugins(self) -> None: + """Load every linked native plugin into the shared graph engine.""" + ... + + def available_plugins(self) -> list[str]: + """List linked plugins without changing runtime state.""" + ... + + def has_native_artifact(self, artifact: str) -> bool: + """Return whether a linked native parser supports this artifact.""" + ... + + def anchor_concepts(self) -> list[tuple[str, list[str]]]: + """List native anchor concepts and member node types.""" + ... + + +class Graph: + """Graph namespace kept separate from the repository lifecycle.""" + + def rag(self) -> Rag: + """Return the GraphRAG extension bound to this graph.""" + ... + + def ingest_native( + self, plugin: str, artifact: str, data: bytes + ) -> dict[str, Any]: + """Ingest plugin bytes and return detailed native mutation statistics.""" + ... + + def node_ids(self) -> list[str]: + """Return all stable node IDs.""" + ... + + def node_types(self) -> list[str]: + """Return distinct node types present in the graph.""" + ... + + def nodes_by_ids(self, node_ids: list[str]) -> list[dict[str, Any]]: + """Materialize selected nodes in caller order.""" + ... + + def link_nodes( + self, node_ids: list[str], data_source: str + ) -> dict[str, Any]: + """Run native linker rules for selected nodes.""" + ... + + def update_node_flags( + self, + node_id: str, + add: int = 0, + remove: int = 0, + set_to: int | None = None, + ) -> bool: + """Update one node's native flag bitset.""" + ... + + def bfs(self, seed_id: str, depth: int = 0, reverse: bool = False) -> list[str]: + """Return IDs reachable through directed breadth-first traversal.""" + ... + + def shortest_paths( + self, start_id: str, end_id: str, max_depth: int = 5 + ) -> list[list[str]]: + """Return shortest undirected paths between two nodes.""" + ... + + def degree(self, node_id: str, direction: str = "both") -> int: + """Return one node's directional degree.""" + ... + + def subgraph_ids( + self, seed_ids: list[str], depth: int = 0 + ) -> tuple[list[str], list[str]]: + """Return node and edge IDs in a seed neighborhood.""" + ... + + def query_node_ids( + self, expression: str, limit: int | None = None, offset: int = 0 + ) -> list[str]: + """Execute a query and return terminal node IDs.""" + ... + + def query_trace_ids( + self, expression: str, limit: int | None = None, offset: int = 0 + ) -> tuple[list[str], list[str], list[str]]: + """Execute a query and return terminal plus trace IDs.""" + ... + + def induced_snapshot(self, node_ids: list[str]) -> dict[str, Any]: + """Return a canonical induced-subgraph snapshot.""" + ... + + def filter_snapshot( + self, + exclude_mask: int = 0, + include_mask: int = 0, + excluded_ids: list[str] = [], + ) -> dict[str, Any]: + """Return a filtered snapshot and exclusion reasons.""" + ... + + def find_anchors(self, concept_name: str) -> list[dict[str, Any]]: + """Find native anchor instances by concept name.""" + ... + + def elevate_snapshot(self, concept_name: str) -> dict[str, Any]: + """Return an elevated graph snapshot and anchor count.""" + ... + + def add_node(self, node: dict[str, Any]) -> int: + """Add or merge one canonical native dictionary.""" + ... + + def add_nodes(self, nodes: list[dict[str, Any]]) -> int: + """Atomically mutate native dictionaries without a JSON round trip.""" + ... + + def add_nodes_json(self, data: bytes) -> int: + """Single-parse fast path for producers that already own a JSON node array. + + Native Python dictionaries should use :meth:`add_nodes`; encoding them + first would add serialization work instead of removing it. + """ + ... + + def hydrate_cas_nodes(self, items: list[tuple[str, str, bytes]]) -> int: + """Hydrate trusted CAS node bytes without Python JSON decoding.""" + ... + + def add_edge(self, edge: dict[str, Any]) -> int: + """Add or merge one canonical relationship dictionary.""" + ... + + def add_edges(self, edges: list[dict[str, Any]]) -> int: + """Atomically mutate native relationship dictionaries.""" + ... + + def add_edges_json(self, data: bytes) -> int: + """Single-parse fast path for an existing JSON relationship array. + + Native Python dictionaries should use :meth:`add_edges`. + """ + ... + + def hydrate_cas_edges(self, items: list[tuple[str, str, bytes]]) -> int: + """Hydrate trusted CAS edge bytes without Python JSON decoding.""" + ... + + def ingest(self, source: str, data: bytes) -> int: + """Ingest one linked native-plugin payload into the shared graph.""" + ... + + def node(self, node_id: str) -> dict[str, Any]: + """Return one node dictionary or raise ``CSTXError(NOT_FOUND)``.""" + ... + + def find_node(self, identifier: str) -> dict[str, Any] | None: + """Resolve by canonical ID, value, then ``extras.name``.""" + ... + + def patch_node_extras(self, node_ids: list[str], patch: dict[str, Any]) -> int: + """Merge contextual fields into selected nodes in native storage.""" + ... + + def create_relationship( + self, + source_id: str, + target_id: str, + relation: str, + sources: list[str] = [], + attrs: dict[str, Any] | None = None, + identity_key: str | None = None, + ) -> dict[str, Any]: + """Create or merge a relationship with Rust-owned identity.""" + ... + + def is_path_expression(self, expression: str) -> bool: + """Classify an expression using the native query parser.""" + ... + + def union_snapshot(self, other: Graph) -> dict[str, Any]: + """Return the canonical native union with another graph.""" + ... + + def difference_snapshot( + self, other: Graph, node_type: str | None = None + ) -> dict[str, Any]: + """Return nodes present only in this graph.""" + ... + + def contains(self, node_id: str) -> bool: + """Check node existence without materializing a node dictionary.""" + ... + + def node_count(self) -> int: + """Return the current number of nodes.""" + ... + + def edge_count(self) -> int: + """Return the current number of relationships.""" + ... + + def stats(self) -> dict[str, dict[str, int]]: + """Return small aggregate counts as a native mapping.""" + ... + + def nodes( + self, + types: list[str] | None = None, + ids: list[str] | None = None, + sources: list[str] | None = None, + flags_all: int = 0, + flags_any: int = 0, + flags_none: int = 0, + limit: int | None = None, + page_size: int = 1024, + order: str = "unspecified", + ) -> NodeCursor: + """Create the normal lazy native-dictionary read path. + + Keyword filters avoid public filter/options wrapper objects. ``page_size`` + bounds incremental materialization and ``order`` accepts ``unspecified``, + ``id_asc``, or ``id_desc``. + """ + ... + + def edges( + self, + source_id: str | None = None, + target_id: str | None = None, + relations: list[str] | None = None, + sources: list[str] | None = None, + limit: int | None = None, + page_size: int = 1024, + order: str = "unspecified", + ) -> EdgeCursor: + """Create a lazy native-dictionary relationship cursor using keywords.""" + ... + + def neighbors( + self, + node_id: str, + direction: str = "out", + limit: int | None = None, + page_size: int = 1024, + order: str = "unspecified", + ) -> NodeCursor: + """Lazily traverse neighboring nodes without an eager Python result list.""" + ... + + def nodes_json(self, node_type: str | None = None) -> bytes: + """Serialize nodes directly in Rust for JSON transport/export workloads. + + Callers that need Python dictionaries should use :meth:`nodes` so they + do not immediately decode these bytes again. + """ + ... + + def edges_json( + self, + source_id: str | None = None, + target_id: str | None = None, + relation: str | None = None, + ) -> bytes: + """Serialize matching edges without constructing Python dictionaries.""" + ... + + def neighbors_json(self, node_id: str, direction: str = "out") -> bytes: + """Serialize neighboring nodes directly for JSON transport/export.""" + ... + + def query( + self, + expression: str, + limit: int | None = None, + page_size: int = 1024, + order: str = "unspecified", + ) -> NodeCursor: + """Execute the graph DSL and return the normal lazy dictionary path.""" + ... + + def query_json(self, expression: str, limit: int | None = None) -> bytes: + """Run the same DSL as :meth:`query` and encode matches directly in Rust.""" + ... + + +class Repository: + """Snapshot/version namespace separated from graph mutation concerns.""" + + def index_graph(self) -> dict[str, Any]: + """Derive canonical content hashes and CAS payloads from the current graph.""" + ... + + def commit( + self, + message: str, + ref_name: str = "main", + metadata: Any | None = None, + ) -> dict[str, Any]: + """Commit current graph content to a named in-memory repository ref.""" + ... + + def diff(self, base_ref: str, head_ref: str) -> dict[str, Any]: + """Return added, removed, and modified IDs between two refs.""" + ... + + def dump(self, compression: str = "zstd1") -> bytes: + """Return a compact snapshot byte container for storage or transfer.""" + ... + + def load(self, data: bytes, compression: str = "zstd1") -> int: + """Replace graph state from a compact snapshot and return bytes consumed.""" + ... + + def dump_json(self) -> bytes: + """Return canonical JSON snapshot bytes for interoperability/debugging.""" + ... + + def load_json(self, data: bytes) -> int: + """Load existing canonical JSON bytes without Python object transcoding.""" + ... + + def snapshot_fingerprint(self) -> str: + """Return a stable hash of canonical snapshot content.""" + ... + + def head(self, ref_name: str = "main") -> str | None: + """Return one named repository ref without exposing Merkle internals.""" + ... + + def refs(self) -> list[tuple[str, str]]: + """List repository refs without exposing raw Merkle primitives.""" + ... + + def validate_ref(self, ref_name: str) -> None: + """Validate a ref and its complete loaded object graph.""" + ... + + def set_ref(self, ref_name: str, commit_hash: str) -> None: + """Set a ref to a loaded commit after native integrity validation.""" + ... + + def delete_ref(self, ref_name: str) -> None: + """Delete a named repository ref.""" + ... + + def fork(self, source: str, target: str) -> str: + """Fork one loaded ref to another name.""" + ... + + def checkout_entries( + self, + ref_name: str, + types: list[str] | None = None, + ) -> dict[str, dict[str, str]]: + """Return pre-hashed entries for a ref and optional element types.""" + ... + + def commit_entries( + self, + entries: dict[str, dict[str, str]], + ref_name: str, + message: str, + metadata: dict[str, Any], + created_at: int, + ) -> dict[str, Any]: + """Commit pre-hashed entries for an external CAS storage adapter.""" + ... + + def commit_delta( + self, + delta_entries: dict[str, dict[str, str]], + removed_node_ids: list[str], + removed_edge_ids: list[str], + ref_name: str, + message: str, + metadata: dict[str, Any], + created_at: int, + ) -> dict[str, Any]: + """Commit a pre-hashed delta for an external CAS storage adapter.""" + ... + + def merge(self, source: str, target: str, created_at: int) -> dict[str, Any]: + """Merge one loaded ref into another.""" + ... + + def merge_base(self, ref_a: str, ref_b: str) -> str | None: + """Return the nearest common ancestor of two loaded refs.""" + ... + + def log(self, ref_name: str, limit: int = 50) -> list[dict[str, Any]]: + """Return commit history for a loaded ref, newest first.""" + ... + + def tree_stats(self, ref_name: str) -> dict[str, int]: + """Return element counts by type for a loaded ref.""" + ... + + def cherry_pick( + self, + source_ref: str, + target_ref: str, + commit_hashes: list[str], + created_at: int, + ) -> dict[str, Any]: + """Apply selected loaded commits to a target ref.""" + ... + + def import_commit(self, hash: str, data: dict[str, Any]) -> None: + """Import one canonical commit object into native repository state.""" + ... + + def export_commit(self, hash: str) -> dict[str, Any] | None: + """Export one canonical commit object from native repository state.""" + ... + + def export_tree_objects( + self, + root_hash: str, + ) -> dict[str, dict[str, dict[str, str]]]: + """Export all canonical Merkle objects reachable from a tree root.""" + ... + + def import_tree_objects( + self, + objects: dict[str, dict[str, dict[str, str]]], + ) -> None: + """Import canonical Merkle objects fetched by a storage adapter.""" + ... + + def tree_entries( + self, + root_hash: str, + types: list[str] | None = None, + ) -> dict[str, dict[str, str]]: + """Return content-hash entries for a loaded tree root.""" + ... + + def find_tree_entry(self, root_hash: str, element_id: str) -> str | None: + """Find one element's content hash in a loaded tree root.""" + ... + + def diff_tree_entries( + self, + base_root: str, + head_root: str, + ) -> dict[str, dict[str, tuple[str | None, str | None]]]: + """Return the raw content-hash diff between two loaded tree roots.""" + ... + + def tree_root_stats(self, root_hash: str) -> dict[str, int]: + """Return element counts by type for a loaded tree root.""" + ... + + +class Rag: + """Graph-owned projection and retrieval planner.""" + + def index(self, request: dict[str, Any]) -> dict[str, Any]: + """Project graph changes into a deterministic recall delta.""" + ... + + def retrieve(self, query: dict[str, Any]) -> RagRetrieval: + """Suspend retrieval until external recall batches are supplied.""" + ... + + +class RagRetrieval: + """Suspended retrieval bound to one graph generation and checkpoint.""" + + def requests(self) -> list[dict[str, Any]]: + """Return recall requests required to complete this retrieval.""" + ... + + def complete(self, batches: list[dict[str, Any]]) -> dict[str, Any]: + """Fuse recall batches and build the structured graph context.""" + ... + + +class Bm25Recall: + """In-memory BM25 recall extension and default lexical implementation.""" + + @property + def id(self) -> str: + """Return the stable extension identifier.""" + ... + + def apply(self, delta: dict[str, Any]) -> dict[str, Any]: + """Atomically apply one graph projection delta.""" + ... + + def recall(self, request: dict[str, Any]) -> dict[str, Any]: + """Return ranked record candidates for one recall request.""" + ... + + +class CSTX: + """Single owner of shared schema, graph and repository state.""" + + def __init__( + self, + project_id: str = "default", + cursor_page_size: int = 1024, + ) -> None: + """Open an in-memory runtime with bounded cursor materialization.""" + ... + + @property + def schemas(self) -> Schemas: + """Return the lightweight schema namespace for this runtime.""" + ... + + @property + def graph(self) -> Graph: + """Return the lightweight graph namespace for this runtime.""" + ... + + @property + def repo(self) -> Repository: + """Return the lightweight repository namespace for this runtime.""" + ... + + @property + def closed(self) -> bool: + """Whether the shared runtime has invalidated retained handles.""" + ... + + @property + def project_id(self) -> str: + """Immutable project namespace supplied at construction.""" + ... + + def close(self) -> None: + """Close shared state and invalidate retained services/cursors.""" + ... + + def last_change(self) -> dict[str, Any]: + """Return IDs changed by the most recent committed mutation.""" + ... + + def __enter__(self) -> CSTX: + """Use the runtime as a context manager without creating another owner.""" + ... + + def __exit__(self, *args: Any) -> None: + """Close the shared runtime when its context exits.""" + ... + + +class NodeFlags: + """Discoverable namespace of engine-compatible integer bit constants. + + Graph APIs still accept ordinary ``int`` values; this class is not a node + flag wrapper and cannot create instances. + """ + + NONE: int + HONEYPOT: int + NOISE: int + FALSE_POSITIVE: int + MANUAL_IGNORED: int + THREAT_PRESENT: int + HISTORIC_VULNERABLE: int + INTERNAL: int + + @staticmethod + def all_mask() -> int: + """Return a mask containing every currently defined node flag.""" + ... + + @staticmethod + def default_exclude_mask() -> int: + """Return the engine's standard default-exclusion mask.""" + ... diff --git a/python/cstxpy/sco_gen.py b/python/python/cstxpy/sco_easm.py similarity index 66% rename from python/cstxpy/sco_gen.py rename to python/python/cstxpy/sco_easm.py index 0a33f26..58380f5 100644 --- a/python/cstxpy/sco_gen.py +++ b/python/python/cstxpy/sco_easm.py @@ -1,28 +1,27 @@ # @generated by cstx-codegen — DO NOT EDIT MANUALLY -from typing import List, Optional, Union +# Schema-only shapes: the high-level cstx SDK mixes in its runtime +# model base (Element/SCO) at the wrapper layer. This module must stay +# free of cstx imports so cstxpy never depends on the high-level package. -from pydantic import BaseModel, Field, ConfigDict - - -class SCOBase(BaseModel): - """Base model for all CSTX SCO node types.""" - - model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) +from __future__ import annotations - cstx_type: str - cstx_id: str +from typing import List, Optional +from pydantic import BaseModel, Field, ConfigDict -class Domain(SCOBase): +class DomainBase(BaseModel): """Generated schema for 'domain' node type.""" - host: str + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + host: str -class Subdomain(SCOBase): +class SubdomainBase(BaseModel): """Generated schema for 'subdomain' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + host: str is_tld: bool = Field(default=False) ttl: int = Field(default=0) @@ -34,10 +33,11 @@ class Subdomain(SCOBase): ns: Optional[List[str]] = Field(default=None) txt: Optional[List[str]] = Field(default=None) - -class Ip(SCOBase): +class IpBase(BaseModel): """Generated schema for 'ip' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + ip: str country: str = Field(default="") area: str = Field(default="") @@ -50,24 +50,27 @@ class Ip(SCOBase): cloud: bool = Field(default=False) waf: bool = Field(default=False) - -class Cidr(SCOBase): +class CidrBase(BaseModel): """Generated schema for 'cidr' node type.""" - cidr: str + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + cidr: str -class Port(SCOBase): +class PortBase(BaseModel): """Generated schema for 'port' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + ip: str port: str protocol: str - -class App(SCOBase): +class AppBase(BaseModel): """Generated schema for 'app' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + app_id: str url: str = Field(default="") frameworks: Optional[List[str]] = Field(default=None) @@ -79,11 +82,16 @@ class App(SCOBase): content_type: str = Field(default="") body_length: int = Field(default=0) header_length: int = Field(default=0) + screenshot_id: str = Field(default="") + screenshot_path: str = Field(default="") + ip: str = Field(default="") + port: str = Field(default="") - -class Url(SCOBase): +class UrlBase(BaseModel): """Generated schema for 'url' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + scheme: str host: str = Field(default="") port: str = Field(default="") @@ -96,10 +104,11 @@ class Url(SCOBase): redirect_url: str = Field(default="") frameworks: Optional[List[str]] = Field(default=None) - -class Framework(SCOBase): +class FrameworkBase(BaseModel): """Generated schema for 'framework' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + name: str part: str = Field(default="") vendor: str = Field(default="") @@ -109,10 +118,11 @@ class Framework(SCOBase): is_focus: bool = Field(default=False) sources: Optional[List[str]] = Field(default=None) - -class Vuln(SCOBase): +class VulnBase(BaseModel): """Generated schema for 'vuln' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + value: str vuln_id: str = Field(default="") name: str = Field(default="") @@ -131,11 +141,33 @@ class Vuln(SCOBase): response: str = Field(default="") username: str = Field(default="") password: str = Field(default="") + matched: bool = Field(default=False) + extracted: bool = Field(default=False) + +class SarifVulnBase(BaseModel): + """Generated schema for 'sarif_vuln' node type.""" + + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + value: str + vuln_id: str = Field(default="") + title: str = Field(default="") + description: str = Field(default="") + source: str = Field(default="") + target: str = Field(default="") + tags: Optional[List[str]] = Field(default=None) + asset_cstx_id: str = Field(default="") + kind: str = Field(default="") + level: str = Field(default="") + baseline_state: str = Field(default="") + rule_id: str = Field(default="") + evidence: str = Field(default="") -class Certificate(SCOBase): +class CertificateBase(BaseModel): """Generated schema for 'certificate' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + fingerprint: str serial: str = Field(default="") issuer: str = Field(default="") @@ -146,20 +178,22 @@ class Certificate(SCOBase): host: str = Field(default="") ip: str = Field(default="") - -class Company(SCOBase): +class CompanyBase(BaseModel): """Generated schema for 'company' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + name: str perc: str = Field(default="") tycid: str = Field(default="") icp: str = Field(default="") parent: str = Field(default="") - -class Icp(SCOBase): +class IcpBase(BaseModel): """Generated schema for 'icp' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + icp: str sub: str = Field(default="") date: str = Field(default="") @@ -168,10 +202,11 @@ class Icp(SCOBase): domain: str = Field(default="") ip: str = Field(default="") - -class Bucket(SCOBase): +class BucketBase(BaseModel): """Generated schema for 'bucket' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + provider: str = Field(default="") name: str = Field(default="") region: str = Field(default="") @@ -181,10 +216,11 @@ class Bucket(SCOBase): known_paths: Optional[List[str]] = Field(default=None) source_url: str = Field(default="") - -class Endpoint(SCOBase): +class EndpointBase(BaseModel): """Generated schema for 'endpoint' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + url: str method: str = Field(default="") path: str = Field(default="") @@ -195,10 +231,11 @@ class Endpoint(SCOBase): parameters: Optional[List[str]] = Field(default=None) tags: Optional[List[str]] = Field(default=None) - -class Host(SCOBase): +class HostBase(BaseModel): """Generated schema for 'host' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + hostname: str local_ips: Optional[List[str]] = Field(default=None) gateway_ips: Optional[List[str]] = Field(default=None) @@ -206,10 +243,11 @@ class Host(SCOBase): domain_name: str = Field(default="") domain_role: str = Field(default="") - -class Repository(SCOBase): +class RepositoryBase(BaseModel): """Generated schema for 'repository' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + provider: str = Field(default="") name: str = Field(default="") url: str @@ -219,10 +257,11 @@ class Repository(SCOBase): is_fork: bool = Field(default=False) matched_dorks: Optional[List[str]] = Field(default=None) - -class Secret(SCOBase): +class SecretBase(BaseModel): """Generated schema for 'secret' node type.""" + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + kind: str = Field(default="") detector: str = Field(default="") redacted: str = Field(default="") @@ -235,83 +274,23 @@ class Secret(SCOBase): verified: bool = Field(default=False) severity: str = Field(default="") - -SCONode = Union[ - Domain, - Subdomain, - Ip, - Cidr, - Port, - App, - Url, - Framework, - Vuln, - Certificate, - Company, - Icp, - Bucket, - Endpoint, - Host, - Repository, - Secret, -] - -SCO_TYPE_MAP: dict[str, type[SCOBase]] = { - "domain": Domain, - "subdomain": Subdomain, - "ip": Ip, - "cidr": Cidr, - "port": Port, - "app": App, - "url": Url, - "framework": Framework, - "vuln": Vuln, - "certificate": Certificate, - "company": Company, - "icp": Icp, - "bucket": Bucket, - "endpoint": Endpoint, - "host": Host, - "repository": Repository, - "secret": Secret, -} - - -def parse_sco_node(data: dict) -> SCOBase: - """Parse a raw dict into the appropriate typed SCO model. - - Dispatches on the 'cstx_type' field to select the correct class. - Raises KeyError if cstx_type is missing or unknown. - """ - cstx_type = data.get("cstx_type") - if cstx_type is None: - raise KeyError("Missing 'cstx_type' field in data") - cls = SCO_TYPE_MAP.get(cstx_type) - if cls is None: - raise KeyError(f"Unknown cstx_type: {cstx_type!r}") - return cls.model_validate(data) - - __all__ = [ - "SCOBase", - "Domain", - "Subdomain", - "Ip", - "Cidr", - "Port", - "App", - "Url", - "Framework", - "Vuln", - "Certificate", - "Company", - "Icp", - "Bucket", - "Endpoint", - "Host", - "Repository", - "Secret", - "SCONode", - "SCO_TYPE_MAP", - "parse_sco_node", + "DomainBase", + "SubdomainBase", + "IpBase", + "CidrBase", + "PortBase", + "AppBase", + "UrlBase", + "FrameworkBase", + "VulnBase", + "SarifVulnBase", + "CertificateBase", + "CompanyBase", + "IcpBase", + "BucketBase", + "EndpointBase", + "HostBase", + "RepositoryBase", + "SecretBase", ] diff --git a/python/python/cstxpy/sro_easm.py b/python/python/cstxpy/sro_easm.py new file mode 100644 index 0000000..a8b4444 --- /dev/null +++ b/python/python/cstxpy/sro_easm.py @@ -0,0 +1,9 @@ +# @generated by cstx-codegen — DO NOT EDIT MANUALLY + +from __future__ import annotations + +from typing import List, Literal, Optional + +RelationType = Literal["resolve", "open", "has-subdomain", "contain", "hosts", "uses", "refers", "secured_by", "exploit", "affect", "invest", "own", "filed-for"] + +RELATION_TYPES: List[RelationType] = ["resolve", "open", "has-subdomain", "contain", "hosts", "uses", "refers", "secured_by", "exploit", "affect", "invest", "own", "filed-for"] diff --git a/python/src/lib.rs b/python/src/lib.rs index 2b87ac3..a4916ee 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,267 +1,7 @@ use pyo3::prelude::*; -use pyo3::types::PyDict; -use std::sync::Arc; - -use cstx_graph::engine::CstxEngine; - -macro_rules! to_pydict { - ($py:expr, $($key:literal => $val:expr),* $(,)?) => {{ - let dict = PyDict::new_bound($py); - $(dict.set_item($key, $val)?;)* - Ok(dict) - }}; -} - -fn to_py_err(e: E) -> PyErr { - pyo3::exceptions::PyRuntimeError::new_err(format!("{e:#}")) -} - -#[pyclass] -struct Graph { - engine: CstxEngine, -} - -#[pymethods] -impl Graph { - #[new] - fn new() -> Self { - Self { engine: CstxEngine::new() } - } - - fn load_easm(&mut self) { - self.engine.register_plugin(Arc::new(cstx_easm::EasmPlugin)); - } - - fn load_plugin(&mut self, name: &str) -> PyResult<()> { - let p = cstx_graph::plugin::get_plugin(name) - .ok_or_else(|| pyo3::exceptions::PyValueError::new_err(format!("plugin '{}' not found", name)))?; - self.engine.register_plugin(p); - Ok(()) - } - - fn load_all_plugins(&mut self) { - for p in cstx_graph::plugin::all_plugins() { - self.engine.register_plugin(p); - } - } - - #[pyo3(signature = (node_type, json_schema_str, value_field=None))] - fn register_schema(&mut self, node_type: &str, json_schema_str: &str, value_field: Option<&str>) -> PyResult<()> { - self.engine.register_schema(node_type, json_schema_str, value_field).map_err(to_py_err) - } - - #[pyo3(signature = (left_type, right_type, relation, left_key, right_key, predicted=false, left_target_id=None, right_source_id=None))] - fn add_join_rule(&mut self, left_type: &str, right_type: &str, relation: &str, left_key: &str, right_key: &str, predicted: bool, left_target_id: Option<&str>, right_source_id: Option<&str>) -> PyResult<()> { - self.engine.add_join_rule(left_type, right_type, relation, left_key, right_key, predicted, left_target_id, right_source_id).map_err(to_py_err) - } - - #[pyo3(signature = (node_type, cstx_id, payload_json, sources, cstx_flags=0, extras_json=None))] - fn add_node(&mut self, node_type: &str, cstx_id: &str, payload_json: &str, sources: Vec, cstx_flags: u64, extras_json: Option<&str>) -> PyResult<()> { - self.engine.add_node(node_type, cstx_id, payload_json, &sources, cstx_flags, extras_json).map_err(to_py_err) - } - - #[pyo3(signature = (source_id, target_id, relation, data_source, attrs_json=None))] - fn add_edge(&mut self, source_id: &str, target_id: &str, relation: &str, data_source: &str, attrs_json: Option<&str>) -> PyResult<()> { - self.engine.add_edge(source_id, target_id, relation, data_source, attrs_json).map_err(to_py_err) - } - - // ── Transform / Link ── - - fn transform(&mut self, source_type: &str, data: &[u8]) -> PyResult { - self.engine.transform(source_type, data).map_err(to_py_err) - } - - fn link<'py>(&mut self, py: Python<'py>, source: &str, data: &[u8]) -> PyResult> { - let r = self.engine.link(source, data).map_err(to_py_err)?; - to_pydict!(py, - "records_parsed" => r.records_parsed, - "new_nodes" => r.link.new_nodes, - "updated_nodes" => r.link.updated_nodes, - "new_edges" => r.link.new_edges, - "node_count" => r.node_count, - "edge_count" => r.edge_count, - ) - } - - // ── Flags ── - - #[pyo3(signature = (node_id, add=0, remove=0, set_to=None))] - fn update_node_flags( - &mut self, - node_id: &str, - add: u64, - remove: u64, - set_to: Option, - ) -> PyResult { - self.engine - .update_node_flags(node_id, add, remove, set_to) - .map(|r| r.is_some()) - .map_err(to_py_err) - } - - // ── Batch ── - - fn add_nodes_batch<'py>(&mut self, py: Python<'py>, nodes_json: &str) -> PyResult> { - let r = self.engine.add_nodes_batch(nodes_json).map_err(to_py_err)?; - to_pydict!(py, "new_nodes" => r.new_nodes, "updated_nodes" => r.updated_nodes, "node_ids" => r.node_ids) - } - - fn add_edges_batch(&mut self, edges_json: &str) -> PyResult { - self.engine.add_edges_batch(edges_json).map_err(to_py_err) - } - - fn ingest_native<'py>(&mut self, py: Python<'py>, plugin_name: &str, artifact: &str, data: &[u8]) -> PyResult> { - let r = self.engine.ingest_native(plugin_name, artifact, data).map_err(to_py_err)?; - to_pydict!(py, "records_parsed" => r.records_parsed, "new_nodes" => r.link.new_nodes, "updated_nodes" => r.link.updated_nodes, "new_edges" => r.link.new_edges, "node_count" => r.node_count, "edge_count" => r.edge_count) - } - - fn has_native_artifact(&self, artifact: &str) -> bool { - self.engine.has_native_artifact(artifact) - } - - #[pyo3(signature = (node_type, data, id_expr, data_source))] - fn ingest_jsonl<'py>(&mut self, py: Python<'py>, node_type: &str, data: &[u8], id_expr: &str, data_source: &str) -> PyResult> { - let r = self.engine.ingest_jsonl(node_type, data, id_expr, data_source).map_err(to_py_err)?; - to_pydict!(py, "records_parsed" => r.records_parsed, "new_nodes" => r.new_nodes, "updated_nodes" => r.updated_nodes) - } - - // ── Accessors ── - - fn contains_node(&self, node_id: &str) -> bool { self.engine.contains_node(node_id) } - fn node_count(&self) -> usize { self.engine.node_count() } - fn edge_count(&self) -> usize { self.engine.edge_count() } - fn node_ids(&self) -> Vec { self.engine.node_ids() } - fn node_types(&self) -> Vec { self.engine.node_types() } - - #[pyo3(signature = (node_type=None))] - fn nodes_json(&self, node_type: Option<&str>) -> String { self.engine.nodes_json(node_type) } - - #[pyo3(signature = (relation=None))] - fn edges_json(&self, relation: Option<&str>) -> String { self.engine.edges_json(relation) } - - fn all_nodes_json(&self) -> String { self.engine.all_nodes_json() } - fn to_json(&self) -> String { self.engine.to_json() } - fn neighbors_json(&self, node_id: &str) -> String { self.engine.neighbors_json(node_id) } - - fn node_payload(&self, node_id: &str) -> Option { self.engine.node_payload(node_id) } - - fn stats<'py>(&self, py: Python<'py>) -> PyResult> { - let s = self.engine.stats(); - let nodes = PyDict::new_bound(py); - for (name, count) in &s.nodes { nodes.set_item(name.as_str(), *count)?; } - let edges = PyDict::new_bound(py); - for (name, count) in &s.edges { edges.set_item(name.as_str(), *count)?; } - to_pydict!(py, "nodes" => nodes, "edges" => edges) - } - - fn version(&self) -> &'static str { env!("CARGO_PKG_VERSION") } - - fn supported_artifacts(&self) -> Vec { - cstx_graph::plugin::all_plugins() - .iter() - .flat_map(|p| p.artifacts().iter().map(|a| a.to_string())) - .collect() - } - - // ── Traversal ── - - #[pyo3(signature = (node_id, direction="out"))] - fn neighbor_ids(&self, node_id: &str, direction: &str) -> Vec { - self.engine.neighbor_ids(node_id, direction) - } - - #[pyo3(signature = (seed_id, depth=0, reverse=false))] - fn bfs(&self, seed_id: &str, depth: u32, reverse: bool) -> Vec { - self.engine.bfs(seed_id, depth, reverse) - } - - #[pyo3(signature = (start_id, end_id, max_depth=5))] - fn shortest_paths(&self, start_id: &str, end_id: &str, max_depth: u32) -> Vec> { - self.engine.shortest_paths(start_id, end_id, max_depth) - } - - #[pyo3(signature = (node_id, direction="both"))] - fn degree(&self, node_id: &str, direction: &str) -> usize { - self.engine.degree(node_id, direction) - } - - #[pyo3(signature = (seed_ids, depth=0))] - fn subgraph_node_ids(&self, seed_ids: Vec, depth: u32) -> (Vec, Vec) { - let refs: Vec<&str> = seed_ids.iter().map(|s| s.as_str()).collect(); - self.engine.subgraph_node_ids(refs, depth) - } - - // ── Query DSL ── - - #[pyo3(signature = (expression,))] - fn parse_query(&self, expression: &str) -> PyResult<(String, Option)> { - let pq = self.engine.parse_query(expression).map_err(to_py_err)?; - let semantic = pq.semantic_query.clone(); - let json = serde_json::to_string(&pq).map_err(to_py_err)?; - Ok((json, semantic)) - } - - #[pyo3(signature = (ast_json, semantic_allowed_ids=None, limit=None, offset=0, flags_exclude_mask=0, flags_include_mask=0))] - fn execute_query( - &self, - ast_json: &str, - semantic_allowed_ids: Option>, - limit: Option, - offset: usize, - flags_exclude_mask: u64, - flags_include_mask: u64, - ) -> PyResult { - self.engine - .execute_query( - ast_json, semantic_allowed_ids.as_deref(), limit, offset, - flags_exclude_mask, flags_include_mask, - ) - .map_err(to_py_err) - } - - #[pyo3(signature = (expression, limit=None, offset=0))] - fn query_dsl(&self, expression: &str, limit: Option, offset: usize) -> PyResult { - self.engine.query_dsl(expression, limit, offset).map_err(to_py_err) - } - - fn is_path_expression(&self, expression: &str) -> bool { - self.engine.is_path_expression(expression) - } - - #[pyo3(signature = (expression, limit=None, offset=0))] - fn query_node_ids(&self, expression: &str, limit: Option, offset: usize) -> PyResult> { - self.engine.query_node_ids(expression, limit, offset).map_err(to_py_err) - } - - // ── Filter / Subgraph ── - - #[pyo3(signature = (source_id=None, target_id=None, relation=None))] - fn edges_filtered(&self, source_id: Option<&str>, target_id: Option<&str>, relation: Option<&str>) -> String { - self.engine.edges_filtered(source_id, target_id, relation) - } - - fn export_snapshot_json(&self) -> String { - self.engine.export_snapshot_json() - } -} - -// ── Module-level transform function ── - -/// Stateless transform: creates a temporary engine with all plugins, -/// ingests data for the given source_type, returns all nodes as JSON. -#[pyfunction] -#[pyo3(name = "transform")] -fn transform_fn(source_type: &str, data: &[u8]) -> PyResult { - let mut engine = CstxEngine::new(); - for p in cstx_graph::plugin::all_plugins() { - engine.register_plugin(p); - } - engine.transform(source_type, data).map_err(to_py_err) -} +use pyo3::types::PyModule; #[pymodule] -fn _cstxpy(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_function(wrap_pyfunction!(transform_fn, m)?)?; - Ok(()) +fn _cstxpy(module: &Bound<'_, PyModule>) -> PyResult<()> { + cstx_core::register_python(module) } diff --git a/python/test_graph.py b/python/test_graph.py deleted file mode 100644 index 0bcc97f..0000000 --- a/python/test_graph.py +++ /dev/null @@ -1,41 +0,0 @@ -from cstxpy import Graph - - -def test_lifecycle(): - g = Graph() - g.load_easm() - assert g.node_count() == 0 - assert g.edge_count() == 0 - - -def test_add_node(): - g = Graph() - g.load_easm() - g.add_node("ip", "ip:10.0.0.1", '{"ip":"10.0.0.1","country":"CN"}', ["test"]) - assert g.node_count() == 1 - assert g.contains_node("ip:10.0.0.1") - - -def test_ingest_native(): - g = Graph() - g.load_easm() - data = b'{"host":"www.example.com","a":["1.2.3.4"],"ttl":300}\n' - result = g.ingest_native("easm", "dnsx", data) - assert result["records_parsed"] == 1 - assert result["new_nodes"] > 0 - - -def test_node_types(): - g = Graph() - g.load_easm() - g.add_node("ip", "ip:1.1.1.1", '{"ip":"1.1.1.1"}', ["test"]) - g.add_node("domain", "domain:a.com", '{"host":"a.com"}', ["test"]) - types = g.node_types() - assert "ip" in types - assert "domain" in types - - -def test_version(): - g = Graph() - v = g.version() - assert v and v != "" diff --git a/python/tests/test_conformance_fixture.py b/python/tests/test_conformance_fixture.py new file mode 100644 index 0000000..6a04841 --- /dev/null +++ b/python/tests/test_conformance_fixture.py @@ -0,0 +1,26 @@ +"""Cross-language checks backed by the canonical v0.3 fixture.""" + +import json +from pathlib import Path + +import cstxpy + + +def test_canonical_v03_fixture_matches_python_contract() -> None: + fixture_path = Path(__file__).resolve().parents[3] / "tests/fixtures/v03_conformance.json" + fixture = json.loads(fixture_path.read_text(encoding="utf-8")) + runtime = cstxpy.CSTX() + schema = fixture["schema"] + runtime.schemas.register( + schema["node_type"], + schema["json_schema"], + schema["value_field"], + ) + runtime.graph.add_nodes(fixture["nodes"]) + runtime.graph.add_edges(fixture["edges"]) + + expected = fixture["expected"] + assert runtime.graph.node_ids() == expected["node_ids"] + assert runtime.graph.node_count() == expected["node_count"] + assert runtime.graph.edge_count() == expected["edge_count"] + assert [node["id"] for node in runtime.graph.query(fixture["query"])] == expected["node_ids"] diff --git a/python/tests/test_graph.py b/python/tests/test_graph.py new file mode 100644 index 0000000..39eadc4 --- /dev/null +++ b/python/tests/test_graph.py @@ -0,0 +1,412 @@ +import ast +import inspect +import json +from pathlib import Path + +import cstxpy +import pytest + +from cstxpy import ( + CSTX, + CSTXError, + EdgeCursor, + Graph, + NodeCursor, + NodeFlags, + Repository, + Schemas, +) + + +SCHEMA = {"properties": {"ip": {"type": "string"}}} + + +def node(value: str, *, flags: int = 0) -> dict: + return { + "id": f"ip:{value}", + "type": "ip", + "value": value, + "model": {"ip": value, "cstx_flags": flags}, + "sources": ["test"], + "extras": {}, + } + + +def edge(source: str, target: str) -> dict: + return { + "id": f"relationship:{source}:related:{target}", + "source_id": source, + "target_id": target, + "relation_type": "related", + "sources": ["test"], + "attrs": {}, + } + + +def db() -> CSTX: + value = CSTX() + value.schemas.register("ip", SCHEMA, "ip") + return value + + +def test_root_services_and_native_values(): + value = db() + assert value.graph.add_node(node("1.1.1.1")) == 1 + assert value.graph.add_node(node("1.1.1.1")) == 0 + assert value.graph.node_count() == 1 + assert value.graph.node("ip:1.1.1.1")["model"]["ip"] == "1.1.1.1" + assert value.last_change()["updated_node_ids"] == [] + + +def test_cursor_filter_order_close_and_invalidation(): + value = db() + value.graph.add_nodes([node("2.2.2.2"), node("1.1.1.1", flags=NodeFlags.HONEYPOT)]) + cursor = value.graph.nodes(order="id_asc") + assert next(cursor)["id"] == "ip:1.1.1.1" + value.graph.add_node(node("3.3.3.3")) + with pytest.raises(CSTXError) as error: + next(cursor) + assert error.value.code == "CURSOR_INVALIDATED" + selected = list(value.graph.nodes(flags_any=NodeFlags.HONEYPOT)) + assert [item["id"] for item in selected] == ["ip:1.1.1.1"] + selected_cursor = value.graph.nodes(limit=1) + selected_cursor.close() + assert selected_cursor.closed + assert list(selected_cursor) == [] + + +def test_prefetched_cursor_page_is_invalidated_before_returning_stale_items(): + value = db() + value.graph.add_nodes([node("1.1.1.1"), node("2.2.2.2"), node("3.3.3.3")]) + cursor = value.graph.nodes(page_size=3, order="id_asc") + assert next(cursor)["id"] == "ip:1.1.1.1" + assert next(cursor)["id"] == "ip:2.2.2.2" + value.graph.add_node(node("4.4.4.4")) + + with pytest.raises(CSTXError) as error: + next(cursor) + assert error.value.code == "CURSOR_INVALIDATED" + + +def test_unchanged_edge_does_not_invalidate_live_cursor(): + value = db() + value.graph.add_nodes([node("1.1.1.1"), node("2.2.2.2"), node("3.3.3.3")]) + relation = edge("ip:1.1.1.1", "ip:2.2.2.2") + value.graph.add_edge(relation) + cursor = value.graph.nodes(page_size=3) + next(cursor) + assert value.graph.add_edge(relation) == 0 + + assert next(cursor)["id"] == "ip:2.2.2.2" + + +def test_edges_neighbors_and_direct_json(): + value = db() + value.graph.add_nodes([node("1.1.1.1"), node("2.2.2.2")]) + relation = edge("ip:1.1.1.1", "ip:2.2.2.2") + assert value.graph.add_edge(relation) == 1 + assert value.graph.edge_count() == 1 + assert list(value.graph.edges())[0]["id"] == relation["id"] + assert list(value.graph.neighbors("ip:1.1.1.1"))[0]["id"] == "ip:2.2.2.2" + assert json.loads(value.graph.edges_json()) == list(value.graph.edges()) + assert json.loads(value.graph.neighbors_json("ip:1.1.1.1")) == list( + value.graph.neighbors("ip:1.1.1.1") + ) + + +def test_native_graph_semantics_own_lookup_context_and_relationship_identity(): + value = db() + first = node("2.2.2.2") + first["extras"] = {"name": "shared", "scope": "old"} + second = node("1.1.1.1") + second["extras"] = {"name": "shared"} + duplicate_value = node("3.3.3.3") + duplicate_value["value"] = "1.1.1.1" + value.graph.add_nodes([first, second, duplicate_value]) + + assert value.graph.find_node("ip:2.2.2.2")["id"] == "ip:2.2.2.2" + assert value.graph.find_node("1.1.1.1")["id"] == "ip:1.1.1.1" + assert value.graph.find_node("shared")["id"] == "ip:1.1.1.1" + assert value.graph.patch_node_extras( + ["ip:2.2.2.2"], {"scope": "new", "run": "r1"} + ) == 1 + assert value.graph.node("ip:2.2.2.2")["extras"] == { + "name": "shared", + "scope": ["old", "new"], + "run": "r1", + } + + relation = value.graph.create_relationship( + "ip:1.1.1.1", + "ip:2.2.2.2", + "related", + ["test"], + {"weight": 1}, + "qualified", + ) + assert relation["id"] == ( + "relationship:ip:1.1.1.1:related:ip:2.2.2.2:qualified" + ) + assert value.graph.is_path_expression("ip -> ip") + assert cstxpy.is_path_expression('ip[country="CN"]') + + +def test_native_graph_union_and_difference_are_deterministic(): + left = db() + right = db() + left.graph.add_nodes([node("1.1.1.1"), node("2.2.2.2")]) + right.graph.add_nodes([node("2.2.2.2"), node("3.3.3.3")]) + + union = left.graph.union_snapshot(right.graph) + assert sorted(item["id"] for item in union["nodes"]) == [ + "ip:1.1.1.1", + "ip:2.2.2.2", + "ip:3.3.3.3", + ] + difference = left.graph.difference_snapshot(right.graph) + assert [item["id"] for item in difference["nodes"]] == ["ip:1.1.1.1"] + assert difference["edges"] == [] + + +def test_trusted_cas_bytes_are_hydrated_natively(): + value = db() + first = node("1.1.1.1") + second = node("2.2.2.2") + first["model"]["__node_type__"] = "ip" + second["model"]["__node_type__"] = "ip" + assert value.graph.hydrate_cas_nodes( + [ + (first["id"], first["type"], json.dumps(first).encode()), + (second["id"], second["type"], json.dumps(second).encode()), + ] + ) == 2 + relation = edge(first["id"], second["id"]) + assert value.graph.hydrate_cas_edges( + [(relation["id"], "edge:related", json.dumps(relation).encode())] + ) == 1 + assert value.graph.node_count() == 2 + assert value.graph.edge_count() == 1 + + +def test_direct_dict_cursor_matches_canonical_json_for_all_column_types(): + value = CSTX(cursor_page_size=2) + value.schemas.register( + "mixed", + { + "properties": { + "name": {"type": "string"}, + "optional": {"type": "string"}, + "count": {"type": "integer"}, + "ratio": {"type": "number"}, + "active": {"type": "boolean"}, + "tags": {"type": "array", "items": {"type": "string"}}, + "ports": {"type": "array", "items": {"type": "integer"}}, + "details": {"type": "object"}, + } + }, + "name", + ) + item = { + "id": "mixed:one", + "type": "mixed", + "value": "one", + "model": { + "name": "one", + "optional": None, + "count": 7, + "ratio": 1.5, + "active": True, + "tags": ["a", "b"], + "ports": [80, 443], + "details": {"nested": [1, 2]}, + }, + "sources": ["test"], + "extras": {"scope": "unit"}, + } + value.graph.add_node(item) + + assert list(value.graph.nodes()) == json.loads(value.graph.nodes_json()) + + +def test_repo_json_round_trip_and_close(): + value = db() + value.graph.add_node(node("1.1.1.1")) + snapshot = value.repo.dump_json() + restored = db() + assert restored.repo.load_json(snapshot) == len(snapshot) + assert restored.graph.node("ip:1.1.1.1")["id"] == "ip:1.1.1.1" + restored.close() + with pytest.raises(CSTXError) as error: + restored.graph.node_count() + assert error.value.code == "NOT_INITIALIZED" + + +def test_not_found_has_stable_error_context(): + value = db() + with pytest.raises(CSTXError) as error: + value.graph.node("ip:missing") + assert error.value.code == "NOT_FOUND" + assert error.value.operation == "graph.node" + + +def test_repository_errors_have_stable_context(): + value = db() + + with pytest.raises(CSTXError) as error: + value.repo.load_json(b"not-json") + assert error.value.code == "CORRUPT_DATA" + assert error.value.operation == "repo.load" + + +def test_every_supported_api_has_runtime_documentation(): + """Keep newly exported APIs explainable through Python help(), not only source.""" + public_members = { + CSTX: ( + "schemas", + "graph", + "repo", + "closed", + "project_id", + "close", + "last_change", + "__enter__", + "__exit__", + ), + Schemas: ( + "register", + "contains", + "get", + "list", + "load_plugin", + "load_all_plugins", + "available_plugins", + "has_native_artifact", + "anchor_concepts", + ), + Graph: ( + "rag", + "add_node", + "add_nodes", + "add_nodes_json", + "hydrate_cas_nodes", + "add_edge", + "add_edges", + "add_edges_json", + "hydrate_cas_edges", + "ingest", + "node", + "find_node", + "patch_node_extras", + "create_relationship", + "is_path_expression", + "union_snapshot", + "difference_snapshot", + "contains", + "node_count", + "edge_count", + "stats", + "nodes", + "edges", + "neighbors", + "nodes_json", + "edges_json", + "neighbors_json", + "query", + "query_json", + "ingest_native", + "node_ids", + "node_types", + "nodes_by_ids", + "link_nodes", + "update_node_flags", + "bfs", + "shortest_paths", + "degree", + "subgraph_ids", + "query_node_ids", + "query_trace_ids", + "induced_snapshot", + "filter_snapshot", + "find_anchors", + "elevate_snapshot", + ), + Repository: ( + "index_graph", + "commit", + "commit_entries", + "commit_delta", + "diff", + "dump", + "load", + "dump_json", + "load_json", + "snapshot_fingerprint", + "head", + "refs", + "validate_ref", + "set_ref", + "delete_ref", + "fork", + "checkout_entries", + "merge", + "merge_base", + "log", + "tree_stats", + "cherry_pick", + "import_commit", + "export_commit", + "import_tree_objects", + "export_tree_objects", + "tree_entries", + "find_tree_entry", + "diff_tree_entries", + "tree_root_stats", + ), + NodeCursor: ("closed", "close", "__iter__", "__next__", "__enter__", "__exit__"), + EdgeCursor: ("closed", "close", "__iter__", "__next__", "__enter__", "__exit__"), + NodeFlags: ("all_mask", "default_exclude_mask"), + } + + assert inspect.getdoc(CSTXError) + for api_type, members in public_members.items(): + assert inspect.getdoc(api_type), api_type.__name__ + for member in members: + assert inspect.getdoc(getattr(api_type, member)), f"{api_type.__name__}.{member}" + discovered = { + name + for name, value in api_type.__dict__.items() + if not name.startswith("_") + and (callable(getattr(api_type, name, None)) or inspect.isdatadescriptor(value)) + } + assert discovered <= set(members), ( + f"undocumented API added to {api_type.__name__}: {discovered - set(members)}" + ) + + +def test_json_api_docs_define_the_fast_path_boundary(): + """JSON suffixes must stay justified as transport paths, not duplicate APIs.""" + for member in ( + Graph.add_nodes_json, + Graph.add_edges_json, + Graph.nodes_json, + Graph.edges_json, + Graph.neighbors_json, + Graph.query_json, + Repository.dump_json, + Repository.load_json, + ): + documentation = inspect.getdoc(member).lower() + assert "json" in documentation + assert any( + reason in documentation + for reason in ("fast path", "transport", "existing", "interoper") + ) + + +def test_type_stub_documents_every_exported_class_and_method(): + """IDE-visible signatures must carry the same explanation as runtime help().""" + stub = Path(cstxpy.__file__).with_name("_cstxpy.pyi") + tree = ast.parse(stub.read_text(encoding="utf-8"), filename=str(stub)) + for node in ast.walk(tree): + if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + assert ast.get_docstring(node), f"missing stub documentation: {node.name}" diff --git a/python/tests/test_json_native.py b/python/tests/test_json_native.py new file mode 100644 index 0000000..4048a8b --- /dev/null +++ b/python/tests/test_json_native.py @@ -0,0 +1,61 @@ +import json + +import pytest + +from cstxpy import CSTX, CSTXError + + +SCHEMA = {"properties": {"ip": {"type": "string"}}} + + +def node(value: str) -> dict: + return { + "id": f"ip:{value}", + "type": "ip", + "value": value, + "model": {"ip": value}, + "sources": ["json"], + "extras": {}, + } + + +def db() -> CSTX: + value = CSTX() + value.schemas.register("ip", SCHEMA, "ip") + return value + + +def test_json_batch_and_direct_nodes_match_native_cursor(): + value = db() + nodes = [node("1.1.1.1"), node("2.2.2.2")] + assert value.graph.add_nodes_json(json.dumps(nodes).encode()) == 2 + assert json.loads(value.graph.nodes_json()) == list(value.graph.nodes()) + + +def test_json_edges_query_neighbors_and_snapshot(): + value = db() + value.graph.add_nodes([node("1.1.1.1"), node("2.2.2.2")]) + relation = { + "id": "relationship:ip:1.1.1.1:related:ip:2.2.2.2", + "source_id": "ip:1.1.1.1", + "target_id": "ip:2.2.2.2", + "relation_type": "related", + "sources": ["json"], + "attrs": {}, + } + assert value.graph.add_edges_json(json.dumps([relation]).encode()) == 1 + assert json.loads(value.graph.edges_json()) == list(value.graph.edges()) + assert json.loads(value.graph.neighbors_json("ip:1.1.1.1")) == list(value.graph.neighbors("ip:1.1.1.1")) + assert json.loads(value.graph.query_json("ip")) == list(value.graph.query("ip")) + snapshot = value.repo.dump_json() + restored = db() + restored.repo.load_json(snapshot) + assert json.loads(restored.graph.nodes_json()) == json.loads(value.graph.nodes_json()) + + +def test_invalid_batch_is_atomic(): + value = db() + bad = [node("1.1.1.1"), {**node("2.2.2.2"), "model": []}] + with pytest.raises(CSTXError): + value.graph.add_nodes_json(json.dumps(bad).encode()) + assert value.graph.node_count() == 0 diff --git a/ts/wasm/cstx_wasm.d.ts b/ts/wasm/cstx_wasm.d.ts index 7987940..a8026a7 100644 --- a/ts/wasm/cstx_wasm.d.ts +++ b/ts/wasm/cstx_wasm.d.ts @@ -1,107 +1,150 @@ /* tslint:disable */ /* eslint-disable */ -export class CSTXGraph { +export class CSTX { free(): void; [Symbol.dispose](): void; - addEdge(source_id: string, target_id: string, relation: string, data_source: string): void; - addEdgesBatch(edges_json: string): number; - addJoinRule(left_type: string, right_type: string, relation: string, left_key: string, right_key: string, predicted: boolean): void; - addNode(node_type: string, cstx_id: string, payload_json: string, sources_json: string, flags: bigint): void; - addNodesBatch(nodes_json: string): string; - allNodesJson(): string; - availablePlugins(): string; - bfs(seed_id: string, depth: number, reverse: boolean): string; - containsNode(node_id: string): boolean; - degree(node_id: string, direction: string): number; - edgeCount(): number; - edgesFiltered(source_id?: string | null, target_id?: string | null, relation?: string | null): string; - edgesJson(relation?: string | null): string; - exportSnapshotJson(): string; - hasNativeArtifact(artifact: string): boolean; - ingestJsonl(node_type: string, data: Uint8Array, id_expr: string, data_source: string): string; - ingestNative(plugin_name: string, artifact: string, data: Uint8Array): string; - isPathExpression(expression: string): boolean; - link(source: string, data: Uint8Array): string; - loadAllPlugins(): number; - loadPlugin(name: string): void; - neighborIds(node_id: string, direction: string): string; - neighborsJson(node_id: string): string; - constructor(); - nodeCount(): number; - nodeIds(): string; - nodePayload(node_id: string): string | undefined; - nodeTypes(): string; - nodesJson(type_filter?: string | null): string; - queryDsl(expression: string, limit: number, offset: number): string; - queryNodeIds(expression: string, limit: number, offset: number): string; - registerSchema(node_type: string, json_schema: string, value_field?: string | null): void; - shortestPaths(start_id: string, end_id: string, max_depth: number): string; - stats(): string; - subgraphNodeIds(seed_ids_json: string, depth: number): string; - supportedArtifacts(): string; - toJson(): string; - updateNodeFlags(node_id: string, add: bigint, remove: bigint, set_to: bigint): boolean; - version(): string; + close(): void; + constructor(config?: any | null); + readonly closed: boolean; + readonly graph: Graph; + readonly repository: Repository; + readonly schemas: Schemas; +} + +export class EdgeCursor { + private constructor(); + free(): void; + [Symbol.dispose](): void; + close(): void; + next(): any; + readonly closed: boolean; +} + +export class Graph { + private constructor(); + free(): void; + [Symbol.dispose](): void; + addEdge(edge: any): bigint; + addEdges(edges: any): bigint; + addEdgesJson(data: Uint8Array): bigint; + addNode(node: any): bigint; + addNodes(nodes: any): bigint; + addNodesJson(data: Uint8Array): bigint; + contains(node_id: string): boolean; + edgeCount(): bigint; + edges(options?: any | null): EdgeCursor; + ingest(source: string, data: Uint8Array): bigint; + neighbors(node_id: string, direction?: string | null, options?: any | null): NodeCursor; + node(node_id: string): any; + nodeCount(): bigint; + nodes(options?: any | null): NodeCursor; + query(expression: string, options?: any | null): NodeCursor; + stats(): any; +} + +export class NodeCursor { + private constructor(); + free(): void; + [Symbol.dispose](): void; + close(): void; + next(): any; + readonly closed: boolean; } -export function cstxTransform(source_type: string, data: Uint8Array): string; +export class Repository { + private constructor(); + free(): void; + [Symbol.dispose](): void; + commit(ref_name: string | null | undefined, message: string, metadata?: any | null): any; + diff(base: string, head: string): any; + dump(compression?: string | null): Uint8Array; + head(ref_name?: string | null): any; + load(data: Uint8Array, compression?: string | null): bigint; + refs(): any; + snapshotFingerprint(): string; +} -export function flagsAllMask(): bigint; +export class Schemas { + private constructor(); + free(): void; + [Symbol.dispose](): void; + availablePlugins(): any; + contains(node_type: string): boolean; + get(node_type: string): any; + list(): any; + loadAllPlugins(): void; + loadPlugin(name: string): void; + register(node_type: string, schema: any, value_field?: string | null): void; +} -export function flagsDefaultExcludeMask(): bigint; +export function version(): string; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { readonly memory: WebAssembly.Memory; - readonly __wbg_cstxgraph_free: (a: number, b: number) => void; - readonly cstxTransform: (a: number, b: number, c: number, d: number, e: number) => void; - readonly cstxgraph_addEdge: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void; - readonly cstxgraph_addEdgesBatch: (a: number, b: number, c: number, d: number) => void; - readonly cstxgraph_addJoinRule: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number) => void; - readonly cstxgraph_addNode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: bigint) => void; - readonly cstxgraph_addNodesBatch: (a: number, b: number, c: number, d: number) => void; - readonly cstxgraph_allNodesJson: (a: number, b: number) => void; - readonly cstxgraph_availablePlugins: (a: number, b: number) => void; - readonly cstxgraph_bfs: (a: number, b: number, c: number, d: number, e: number, f: number) => void; - readonly cstxgraph_containsNode: (a: number, b: number, c: number) => number; - readonly cstxgraph_degree: (a: number, b: number, c: number, d: number, e: number) => number; - readonly cstxgraph_edgeCount: (a: number) => number; - readonly cstxgraph_edgesFiltered: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; - readonly cstxgraph_edgesJson: (a: number, b: number, c: number, d: number) => void; - readonly cstxgraph_exportSnapshotJson: (a: number, b: number) => void; - readonly cstxgraph_hasNativeArtifact: (a: number, b: number, c: number) => number; - readonly cstxgraph_ingestJsonl: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void; - readonly cstxgraph_ingestNative: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; - readonly cstxgraph_isPathExpression: (a: number, b: number, c: number) => number; - readonly cstxgraph_link: (a: number, b: number, c: number, d: number, e: number, f: number) => void; - readonly cstxgraph_loadAllPlugins: (a: number) => number; - readonly cstxgraph_loadPlugin: (a: number, b: number, c: number, d: number) => void; - readonly cstxgraph_neighborIds: (a: number, b: number, c: number, d: number, e: number, f: number) => void; - readonly cstxgraph_neighborsJson: (a: number, b: number, c: number, d: number) => void; - readonly cstxgraph_new: () => number; - readonly cstxgraph_nodeCount: (a: number) => number; - readonly cstxgraph_nodeIds: (a: number, b: number) => void; - readonly cstxgraph_nodePayload: (a: number, b: number, c: number, d: number) => void; - readonly cstxgraph_nodeTypes: (a: number, b: number) => void; - readonly cstxgraph_nodesJson: (a: number, b: number, c: number, d: number) => void; - readonly cstxgraph_queryDsl: (a: number, b: number, c: number, d: number, e: number, f: number) => void; - readonly cstxgraph_queryNodeIds: (a: number, b: number, c: number, d: number, e: number, f: number) => void; - readonly cstxgraph_registerSchema: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; - readonly cstxgraph_shortestPaths: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; - readonly cstxgraph_stats: (a: number, b: number) => void; - readonly cstxgraph_subgraphNodeIds: (a: number, b: number, c: number, d: number, e: number) => void; - readonly cstxgraph_supportedArtifacts: (a: number, b: number) => void; - readonly cstxgraph_toJson: (a: number, b: number) => void; - readonly cstxgraph_updateNodeFlags: (a: number, b: number, c: number, d: number, e: bigint, f: bigint, g: bigint) => void; - readonly cstxgraph_version: (a: number, b: number) => void; - readonly flagsAllMask: () => bigint; - readonly flagsDefaultExcludeMask: () => bigint; - readonly __wbindgen_export: (a: number) => void; + readonly __wbg_cstx_free: (a: number, b: number) => void; + readonly __wbg_edgecursor_free: (a: number, b: number) => void; + readonly __wbg_graph_free: (a: number, b: number) => void; + readonly __wbg_nodecursor_free: (a: number, b: number) => void; + readonly cstx_close: (a: number) => void; + readonly cstx_closed: (a: number) => number; + readonly cstx_graph: (a: number) => number; + readonly cstx_new: (a: number, b: number) => void; + readonly edgecursor_close: (a: number) => void; + readonly edgecursor_closed: (a: number) => number; + readonly edgecursor_next: (a: number, b: number) => void; + readonly graph_addEdge: (a: number, b: number, c: number) => void; + readonly graph_addEdges: (a: number, b: number, c: number) => void; + readonly graph_addEdgesJson: (a: number, b: number, c: number, d: number) => void; + readonly graph_addNode: (a: number, b: number, c: number) => void; + readonly graph_addNodes: (a: number, b: number, c: number) => void; + readonly graph_addNodesJson: (a: number, b: number, c: number, d: number) => void; + readonly graph_contains: (a: number, b: number, c: number, d: number) => void; + readonly graph_edgeCount: (a: number, b: number) => void; + readonly graph_edges: (a: number, b: number, c: number) => void; + readonly graph_ingest: (a: number, b: number, c: number, d: number, e: number, f: number) => void; + readonly graph_neighbors: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; + readonly graph_node: (a: number, b: number, c: number, d: number) => void; + readonly graph_nodeCount: (a: number, b: number) => void; + readonly graph_nodes: (a: number, b: number, c: number) => void; + readonly graph_query: (a: number, b: number, c: number, d: number, e: number) => void; + readonly graph_stats: (a: number, b: number) => void; + readonly nodecursor_close: (a: number) => void; + readonly nodecursor_closed: (a: number) => number; + readonly nodecursor_next: (a: number, b: number) => void; + readonly repository_commit: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; + readonly repository_diff: (a: number, b: number, c: number, d: number, e: number, f: number) => void; + readonly repository_dump: (a: number, b: number, c: number, d: number) => void; + readonly repository_head: (a: number, b: number, c: number, d: number) => void; + readonly repository_load: (a: number, b: number, c: number, d: number, e: number, f: number) => void; + readonly repository_refs: (a: number, b: number) => void; + readonly repository_snapshotFingerprint: (a: number, b: number) => void; + readonly schemas_availablePlugins: (a: number, b: number) => void; + readonly schemas_contains: (a: number, b: number, c: number, d: number) => void; + readonly schemas_get: (a: number, b: number, c: number, d: number) => void; + readonly schemas_list: (a: number, b: number) => void; + readonly schemas_loadAllPlugins: (a: number, b: number) => void; + readonly schemas_loadPlugin: (a: number, b: number, c: number, d: number) => void; + readonly schemas_register: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; + readonly version: (a: number) => void; + readonly rust_zstd_wasm_shim_calloc: (a: number, b: number) => number; + readonly rust_zstd_wasm_shim_free: (a: number) => void; + readonly rust_zstd_wasm_shim_malloc: (a: number) => number; + readonly rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number; + readonly rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number; + readonly rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number; + readonly rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number; + readonly rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void; + readonly cstx_repository: (a: number) => number; + readonly cstx_schemas: (a: number) => number; + readonly __wbg_repository_free: (a: number, b: number) => void; + readonly __wbg_schemas_free: (a: number, b: number) => void; + readonly __wbindgen_export: (a: number, b: number) => number; + readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_export3: (a: number) => void; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; - readonly __wbindgen_export2: (a: number, b: number) => number; - readonly __wbindgen_export3: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_export4: (a: number, b: number, c: number) => void; } diff --git a/ts/wasm/cstx_wasm.js b/ts/wasm/cstx_wasm.js index d1e612d..0540abd 100644 --- a/ts/wasm/cstx_wasm.js +++ b/ts/wasm/cstx_wasm.js @@ -1,898 +1,862 @@ /* @ts-self-types="./cstx_wasm.d.ts" */ -export class CSTXGraph { +export class CSTX { __destroy_into_raw() { const ptr = this.__wbg_ptr; this.__wbg_ptr = 0; - CSTXGraphFinalization.unregister(this); + CSTXFinalization.unregister(this); return ptr; } free() { const ptr = this.__destroy_into_raw(); - wasm.__wbg_cstxgraph_free(ptr, 0); + wasm.__wbg_cstx_free(ptr, 0); + } + close() { + wasm.cstx_close(this.__wbg_ptr); + } + /** + * @returns {boolean} + */ + get closed() { + const ret = wasm.cstx_closed(this.__wbg_ptr); + return ret !== 0; + } + /** + * @returns {Graph} + */ + get graph() { + const ret = wasm.cstx_graph(this.__wbg_ptr); + return Graph.__wrap(ret); } /** - * @param {string} source_id - * @param {string} target_id - * @param {string} relation - * @param {string} data_source + * @param {any | null} [config] */ - addEdge(source_id, target_id, relation, data_source) { + constructor(config) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(source_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(target_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len1 = WASM_VECTOR_LEN; - const ptr2 = passStringToWasm0(relation, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len2 = WASM_VECTOR_LEN; - const ptr3 = passStringToWasm0(data_source, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len3 = WASM_VECTOR_LEN; - wasm.cstxgraph_addEdge(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3); + wasm.cstx_new(retptr, isLikeNone(config) ? 0 : addHeapObject(config)); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - if (r1) { - throw takeObject(r0); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); } + this.__wbg_ptr = r0; + CSTXFinalization.register(this, this.__wbg_ptr, this); + return this; } finally { wasm.__wbindgen_add_to_stack_pointer(16); } } /** - * @param {string} edges_json - * @returns {number} + * @returns {Repository} + */ + get repository() { + const ret = wasm.cstx_repository(this.__wbg_ptr); + return Repository.__wrap(ret); + } + /** + * @returns {Schemas} + */ + get schemas() { + const ret = wasm.cstx_schemas(this.__wbg_ptr); + return Schemas.__wrap(ret); + } +} +if (Symbol.dispose) CSTX.prototype[Symbol.dispose] = CSTX.prototype.free; + +export class EdgeCursor { + static __wrap(ptr) { + const obj = Object.create(EdgeCursor.prototype); + obj.__wbg_ptr = ptr; + EdgeCursorFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + EdgeCursorFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_edgecursor_free(ptr, 0); + } + close() { + wasm.edgecursor_close(this.__wbg_ptr); + } + /** + * @returns {boolean} + */ + get closed() { + const ret = wasm.edgecursor_closed(this.__wbg_ptr); + return ret !== 0; + } + /** + * @returns {any} */ - addEdgesBatch(edges_json) { + next() { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(edges_json, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_addEdgesBatch(retptr, this.__wbg_ptr, ptr0, len0); + wasm.edgecursor_next(retptr, this.__wbg_ptr); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); if (r2) { throw takeObject(r1); } - return r0 >>> 0; + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); } } +} +if (Symbol.dispose) EdgeCursor.prototype[Symbol.dispose] = EdgeCursor.prototype.free; + +export class Graph { + static __wrap(ptr) { + const obj = Object.create(Graph.prototype); + obj.__wbg_ptr = ptr; + GraphFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + GraphFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_graph_free(ptr, 0); + } /** - * @param {string} left_type - * @param {string} right_type - * @param {string} relation - * @param {string} left_key - * @param {string} right_key - * @param {boolean} predicted + * @param {any} edge + * @returns {bigint} */ - addJoinRule(left_type, right_type, relation, left_key, right_key, predicted) { + addEdge(edge) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(left_type, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(right_type, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len1 = WASM_VECTOR_LEN; - const ptr2 = passStringToWasm0(relation, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len2 = WASM_VECTOR_LEN; - const ptr3 = passStringToWasm0(left_key, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len3 = WASM_VECTOR_LEN; - const ptr4 = passStringToWasm0(right_key, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len4 = WASM_VECTOR_LEN; - wasm.cstxgraph_addJoinRule(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, predicted); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - if (r1) { - throw takeObject(r0); + wasm.graph_addEdge(retptr, this.__wbg_ptr, addHeapObject(edge)); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); } + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); } } /** - * @param {string} node_type - * @param {string} cstx_id - * @param {string} payload_json - * @param {string} sources_json - * @param {bigint} flags + * @param {any} edges + * @returns {bigint} */ - addNode(node_type, cstx_id, payload_json, sources_json, flags) { + addEdges(edges) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(node_type, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(cstx_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len1 = WASM_VECTOR_LEN; - const ptr2 = passStringToWasm0(payload_json, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len2 = WASM_VECTOR_LEN; - const ptr3 = passStringToWasm0(sources_json, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len3 = WASM_VECTOR_LEN; - wasm.cstxgraph_addNode(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, flags); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - if (r1) { - throw takeObject(r0); + wasm.graph_addEdges(retptr, this.__wbg_ptr, addHeapObject(edges)); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); } + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); } } /** - * @param {string} nodes_json - * @returns {string} + * @param {Uint8Array} data + * @returns {bigint} */ - addNodesBatch(nodes_json) { - let deferred3_0; - let deferred3_1; + addEdgesJson(data) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(nodes_json, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export); const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_addNodesBatch(retptr, this.__wbg_ptr, ptr0, len0); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + wasm.graph_addEdgesJson(retptr, this.__wbg_ptr, ptr0, len0); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); - var ptr2 = r0; - var len2 = r1; if (r3) { - ptr2 = 0; len2 = 0; throw takeObject(r2); } - deferred3_0 = ptr2; - deferred3_1 = len2; - return getStringFromWasm0(ptr2, len2); + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1); } } /** - * @returns {string} + * @param {any} node + * @returns {bigint} */ - allNodesJson() { - let deferred1_0; - let deferred1_1; + addNode(node) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_allNodesJson(retptr, this.__wbg_ptr); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + wasm.graph_addNode(retptr, this.__wbg_ptr, addHeapObject(node)); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } /** - * @returns {string} + * @param {any} nodes + * @returns {bigint} */ - availablePlugins() { - let deferred1_0; - let deferred1_1; + addNodes(nodes) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_availablePlugins(retptr, this.__wbg_ptr); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + wasm.graph_addNodes(retptr, this.__wbg_ptr, addHeapObject(nodes)); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } /** - * @param {string} seed_id - * @param {number} depth - * @param {boolean} reverse - * @returns {string} + * @param {Uint8Array} data + * @returns {bigint} */ - bfs(seed_id, depth, reverse) { - let deferred2_0; - let deferred2_1; + addNodesJson(data) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(seed_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export); const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_bfs(retptr, this.__wbg_ptr, ptr0, len0, depth, reverse); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred2_0 = r0; - deferred2_1 = r1; - return getStringFromWasm0(r0, r1); + wasm.graph_addNodesJson(retptr, this.__wbg_ptr, ptr0, len0); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1); } } /** * @param {string} node_id * @returns {boolean} */ - containsNode(node_id) { - const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.cstxgraph_containsNode(this.__wbg_ptr, ptr0, len0); - return ret !== 0; - } - /** - * @param {string} node_id - * @param {string} direction - * @returns {number} - */ - degree(node_id, direction) { - const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(direction, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len1 = WASM_VECTOR_LEN; - const ret = wasm.cstxgraph_degree(this.__wbg_ptr, ptr0, len0, ptr1, len1); - return ret >>> 0; - } - /** - * @returns {number} - */ - edgeCount() { - const ret = wasm.cstxgraph_edgeCount(this.__wbg_ptr); - return ret >>> 0; - } - /** - * @param {string | null} [source_id] - * @param {string | null} [target_id] - * @param {string | null} [relation] - * @returns {string} - */ - edgesFiltered(source_id, target_id, relation) { - let deferred4_0; - let deferred4_1; + contains(node_id) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - var ptr0 = isLikeNone(source_id) ? 0 : passStringToWasm0(source_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - var len0 = WASM_VECTOR_LEN; - var ptr1 = isLikeNone(target_id) ? 0 : passStringToWasm0(target_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - var len1 = WASM_VECTOR_LEN; - var ptr2 = isLikeNone(relation) ? 0 : passStringToWasm0(relation, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - var len2 = WASM_VECTOR_LEN; - wasm.cstxgraph_edgesFiltered(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2); + const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + wasm.graph_contains(retptr, this.__wbg_ptr, ptr0, len0); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred4_0 = r0; - deferred4_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return r0 !== 0; } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred4_0, deferred4_1, 1); } } /** - * @param {string | null} [relation] - * @returns {string} + * @returns {bigint} */ - edgesJson(relation) { - let deferred2_0; - let deferred2_1; + edgeCount() { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - var ptr0 = isLikeNone(relation) ? 0 : passStringToWasm0(relation, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - var len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_edgesJson(retptr, this.__wbg_ptr, ptr0, len0); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred2_0 = r0; - deferred2_1 = r1; - return getStringFromWasm0(r0, r1); + wasm.graph_edgeCount(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1); } } /** - * @returns {string} + * @param {any | null} [options] + * @returns {EdgeCursor} */ - exportSnapshotJson() { - let deferred1_0; - let deferred1_1; + edges(options) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_exportSnapshotJson(retptr, this.__wbg_ptr); + wasm.graph_edges(retptr, this.__wbg_ptr, isLikeNone(options) ? 0 : addHeapObject(options)); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return EdgeCursor.__wrap(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } /** - * @param {string} artifact - * @returns {boolean} - */ - hasNativeArtifact(artifact) { - const ptr0 = passStringToWasm0(artifact, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.cstxgraph_hasNativeArtifact(this.__wbg_ptr, ptr0, len0); - return ret !== 0; - } - /** - * @param {string} node_type + * @param {string} source * @param {Uint8Array} data - * @param {string} id_expr - * @param {string} data_source - * @returns {string} + * @returns {bigint} */ - ingestJsonl(node_type, data, id_expr, data_source) { - let deferred6_0; - let deferred6_1; + ingest(source, data) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(node_type, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passStringToWasm0(source, wasm.__wbindgen_export, wasm.__wbindgen_export2); const len0 = WASM_VECTOR_LEN; - const ptr1 = passArray8ToWasm0(data, wasm.__wbindgen_export2); + const ptr1 = passArray8ToWasm0(data, wasm.__wbindgen_export); const len1 = WASM_VECTOR_LEN; - const ptr2 = passStringToWasm0(id_expr, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len2 = WASM_VECTOR_LEN; - const ptr3 = passStringToWasm0(data_source, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len3 = WASM_VECTOR_LEN; - wasm.cstxgraph_ingestJsonl(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + wasm.graph_ingest(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); - var ptr5 = r0; - var len5 = r1; if (r3) { - ptr5 = 0; len5 = 0; throw takeObject(r2); } - deferred6_0 = ptr5; - deferred6_1 = len5; - return getStringFromWasm0(ptr5, len5); + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred6_0, deferred6_1, 1); } } /** - * @param {string} plugin_name - * @param {string} artifact - * @param {Uint8Array} data - * @returns {string} + * @param {string} node_id + * @param {string | null} [direction] + * @param {any | null} [options] + * @returns {NodeCursor} */ - ingestNative(plugin_name, artifact, data) { - let deferred5_0; - let deferred5_1; + neighbors(node_id, direction, options) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(plugin_name, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export, wasm.__wbindgen_export2); const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(artifact, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len1 = WASM_VECTOR_LEN; - const ptr2 = passArray8ToWasm0(data, wasm.__wbindgen_export2); - const len2 = WASM_VECTOR_LEN; - wasm.cstxgraph_ingestNative(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2); + var ptr1 = isLikeNone(direction) ? 0 : passStringToWasm0(direction, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + wasm.graph_neighbors(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, isLikeNone(options) ? 0 : addHeapObject(options)); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); - var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); - var ptr4 = r0; - var len4 = r1; - if (r3) { - ptr4 = 0; len4 = 0; - throw takeObject(r2); + if (r2) { + throw takeObject(r1); } - deferred5_0 = ptr4; - deferred5_1 = len4; - return getStringFromWasm0(ptr4, len4); + return NodeCursor.__wrap(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred5_0, deferred5_1, 1); } } /** - * @param {string} expression - * @returns {boolean} - */ - isPathExpression(expression) { - const ptr0 = passStringToWasm0(expression, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.cstxgraph_isPathExpression(this.__wbg_ptr, ptr0, len0); - return ret !== 0; - } - /** - * @param {string} source - * @param {Uint8Array} data - * @returns {string} + * @param {string} node_id + * @returns {any} */ - link(source, data) { - let deferred4_0; - let deferred4_1; + node(node_id) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(source, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export, wasm.__wbindgen_export2); const len0 = WASM_VECTOR_LEN; - const ptr1 = passArray8ToWasm0(data, wasm.__wbindgen_export2); - const len1 = WASM_VECTOR_LEN; - wasm.cstxgraph_link(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1); + wasm.graph_node(retptr, this.__wbg_ptr, ptr0, len0); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); - var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); - var ptr3 = r0; - var len3 = r1; - if (r3) { - ptr3 = 0; len3 = 0; - throw takeObject(r2); + if (r2) { + throw takeObject(r1); } - deferred4_0 = ptr3; - deferred4_1 = len3; - return getStringFromWasm0(ptr3, len3); + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred4_0, deferred4_1, 1); } } /** - * @returns {number} + * @returns {bigint} */ - loadAllPlugins() { - const ret = wasm.cstxgraph_loadAllPlugins(this.__wbg_ptr); - return ret >>> 0; + nodeCount() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.graph_nodeCount(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + return BigInt.asUintN(64, r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } } /** - * @param {string} name + * @param {any | null} [options] + * @returns {NodeCursor} */ - loadPlugin(name) { + nodes(options) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(name, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_loadPlugin(retptr, this.__wbg_ptr, ptr0, len0); + wasm.graph_nodes(retptr, this.__wbg_ptr, isLikeNone(options) ? 0 : addHeapObject(options)); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - if (r1) { - throw takeObject(r0); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); } + return NodeCursor.__wrap(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); } } /** - * @param {string} node_id - * @param {string} direction - * @returns {string} + * @param {string} expression + * @param {any | null} [options] + * @returns {NodeCursor} */ - neighborIds(node_id, direction) { - let deferred3_0; - let deferred3_1; + query(expression, options) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passStringToWasm0(expression, wasm.__wbindgen_export, wasm.__wbindgen_export2); const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(direction, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len1 = WASM_VECTOR_LEN; - wasm.cstxgraph_neighborIds(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1); + wasm.graph_query(retptr, this.__wbg_ptr, ptr0, len0, isLikeNone(options) ? 0 : addHeapObject(options)); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred3_0 = r0; - deferred3_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return NodeCursor.__wrap(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1); } } /** - * @param {string} node_id - * @returns {string} + * @returns {any} */ - neighborsJson(node_id) { - let deferred2_0; - let deferred2_1; + stats() { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_neighborsJson(retptr, this.__wbg_ptr, ptr0, len0); + wasm.graph_stats(retptr, this.__wbg_ptr); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred2_0 = r0; - deferred2_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1); } } - constructor() { - const ret = wasm.cstxgraph_new(); - this.__wbg_ptr = ret; - CSTXGraphFinalization.register(this, this.__wbg_ptr, this); - return this; +} +if (Symbol.dispose) Graph.prototype[Symbol.dispose] = Graph.prototype.free; + +export class NodeCursor { + static __wrap(ptr) { + const obj = Object.create(NodeCursor.prototype); + obj.__wbg_ptr = ptr; + NodeCursorFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + NodeCursorFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_nodecursor_free(ptr, 0); + } + close() { + wasm.nodecursor_close(this.__wbg_ptr); } /** - * @returns {number} + * @returns {boolean} */ - nodeCount() { - const ret = wasm.cstxgraph_nodeCount(this.__wbg_ptr); - return ret >>> 0; + get closed() { + const ret = wasm.nodecursor_closed(this.__wbg_ptr); + return ret !== 0; } /** - * @returns {string} + * @returns {any} */ - nodeIds() { - let deferred1_0; - let deferred1_1; + next() { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_nodeIds(retptr, this.__wbg_ptr); + wasm.nodecursor_next(retptr, this.__wbg_ptr); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } +} +if (Symbol.dispose) NodeCursor.prototype[Symbol.dispose] = NodeCursor.prototype.free; + +export class Repository { + static __wrap(ptr) { + const obj = Object.create(Repository.prototype); + obj.__wbg_ptr = ptr; + RepositoryFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + RepositoryFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_repository_free(ptr, 0); + } /** - * @param {string} node_id - * @returns {string | undefined} + * @param {string | null | undefined} ref_name + * @param {string} message + * @param {any | null} [metadata] + * @returns {any} */ - nodePayload(node_id) { + commit(ref_name, message, metadata) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_nodePayload(retptr, this.__wbg_ptr, ptr0, len0); + var ptr0 = isLikeNone(ref_name) ? 0 : passStringToWasm0(ref_name, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(message, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + wasm.repository_commit(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, isLikeNone(metadata) ? 0 : addHeapObject(metadata)); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - let v2; - if (r0 !== 0) { - v2 = getStringFromWasm0(r0, r1).slice(); - wasm.__wbindgen_export4(r0, r1 * 1, 1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); } - return v2; + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); } } /** - * @returns {string} + * @param {string} base + * @param {string} head + * @returns {any} */ - nodeTypes() { - let deferred1_0; - let deferred1_1; + diff(base, head) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_nodeTypes(retptr, this.__wbg_ptr); + const ptr0 = passStringToWasm0(base, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(head, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + wasm.repository_diff(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } /** - * @param {string | null} [type_filter] - * @returns {string} + * @param {string | null} [compression] + * @returns {Uint8Array} */ - nodesJson(type_filter) { - let deferred2_0; - let deferred2_1; + dump(compression) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - var ptr0 = isLikeNone(type_filter) ? 0 : passStringToWasm0(type_filter, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + var ptr0 = isLikeNone(compression) ? 0 : passStringToWasm0(compression, wasm.__wbindgen_export, wasm.__wbindgen_export2); var len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_nodesJson(retptr, this.__wbg_ptr, ptr0, len0); + wasm.repository_dump(retptr, this.__wbg_ptr, ptr0, len0); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred2_0 = r0; - deferred2_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + var v2 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_export4(r0, r1 * 1, 1); + return v2; } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1); } } /** - * @param {string} expression - * @param {number} limit - * @param {number} offset - * @returns {string} + * @param {string | null} [ref_name] + * @returns {any} */ - queryDsl(expression, limit, offset) { - let deferred3_0; - let deferred3_1; + head(ref_name) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(expression, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_queryDsl(retptr, this.__wbg_ptr, ptr0, len0, limit, offset); + var ptr0 = isLikeNone(ref_name) ? 0 : passStringToWasm0(ref_name, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len0 = WASM_VECTOR_LEN; + wasm.repository_head(retptr, this.__wbg_ptr, ptr0, len0); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); - var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); - var ptr2 = r0; - var len2 = r1; - if (r3) { - ptr2 = 0; len2 = 0; - throw takeObject(r2); + if (r2) { + throw takeObject(r1); } - deferred3_0 = ptr2; - deferred3_1 = len2; - return getStringFromWasm0(ptr2, len2); + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1); } } /** - * @param {string} expression - * @param {number} limit - * @param {number} offset - * @returns {string} + * @param {Uint8Array} data + * @param {string | null} [compression] + * @returns {bigint} */ - queryNodeIds(expression, limit, offset) { - let deferred3_0; - let deferred3_1; + load(data, compression) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(expression, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export); const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_queryNodeIds(retptr, this.__wbg_ptr, ptr0, len0, limit, offset); - var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); - var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var ptr1 = isLikeNone(compression) ? 0 : passStringToWasm0(compression, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + wasm.repository_load(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1); + var r0 = getDataViewMemory0().getBigInt64(retptr + 8 * 0, true); var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); - var ptr2 = r0; - var len2 = r1; if (r3) { - ptr2 = 0; len2 = 0; throw takeObject(r2); } - deferred3_0 = ptr2; - deferred3_1 = len2; - return getStringFromWasm0(ptr2, len2); + return BigInt.asUintN(64, r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1); } } /** - * @param {string} node_type - * @param {string} json_schema - * @param {string | null} [value_field] + * @returns {any} */ - registerSchema(node_type, json_schema, value_field) { + refs() { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(node_type, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(json_schema, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len1 = WASM_VECTOR_LEN; - var ptr2 = isLikeNone(value_field) ? 0 : passStringToWasm0(value_field, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - var len2 = WASM_VECTOR_LEN; - wasm.cstxgraph_registerSchema(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2); + wasm.repository_refs(retptr, this.__wbg_ptr); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - if (r1) { - throw takeObject(r0); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); } + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); } } /** - * @param {string} start_id - * @param {string} end_id - * @param {number} max_depth * @returns {string} */ - shortestPaths(start_id, end_id, max_depth) { - let deferred3_0; - let deferred3_1; + snapshotFingerprint() { + let deferred2_0; + let deferred2_1; try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(start_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(end_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len1 = WASM_VECTOR_LEN; - wasm.cstxgraph_shortestPaths(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, max_depth); + wasm.repository_snapshotFingerprint(retptr, this.__wbg_ptr); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred3_0 = r0; - deferred3_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + var ptr1 = r0; + var len1 = r1; + if (r3) { + ptr1 = 0; len1 = 0; + throw takeObject(r2); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1); + wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1); } } +} +if (Symbol.dispose) Repository.prototype[Symbol.dispose] = Repository.prototype.free; + +export class Schemas { + static __wrap(ptr) { + const obj = Object.create(Schemas.prototype); + obj.__wbg_ptr = ptr; + SchemasFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + SchemasFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_schemas_free(ptr, 0); + } /** - * @returns {string} + * @returns {any} */ - stats() { - let deferred1_0; - let deferred1_1; + availablePlugins() { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_stats(retptr, this.__wbg_ptr); + wasm.schemas_availablePlugins(retptr, this.__wbg_ptr); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } /** - * @param {string} seed_ids_json - * @param {number} depth - * @returns {string} + * @param {string} node_type + * @returns {boolean} */ - subgraphNodeIds(seed_ids_json, depth) { - let deferred2_0; - let deferred2_1; + contains(node_type) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(seed_ids_json, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passStringToWasm0(node_type, wasm.__wbindgen_export, wasm.__wbindgen_export2); const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_subgraphNodeIds(retptr, this.__wbg_ptr, ptr0, len0, depth); + wasm.schemas_contains(retptr, this.__wbg_ptr, ptr0, len0); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred2_0 = r0; - deferred2_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return r0 !== 0; } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1); } } /** - * @returns {string} + * @param {string} node_type + * @returns {any} */ - supportedArtifacts() { - let deferred1_0; - let deferred1_1; + get(node_type) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_supportedArtifacts(retptr, this.__wbg_ptr); + const ptr0 = passStringToWasm0(node_type, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + wasm.schemas_get(retptr, this.__wbg_ptr, ptr0, len0); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } /** - * @returns {string} + * @returns {any} */ - toJson() { - let deferred1_0; - let deferred1_1; + list() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.schemas_list(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + loadAllPlugins() { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_toJson(retptr, this.__wbg_ptr); + wasm.schemas_loadAllPlugins(retptr, this.__wbg_ptr); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + if (r1) { + throw takeObject(r0); + } } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } /** - * @param {string} node_id - * @param {bigint} add - * @param {bigint} remove - * @param {bigint} set_to - * @returns {boolean} + * @param {string} name */ - updateNodeFlags(node_id, add, remove, set_to) { + loadPlugin(name) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(node_id, wasm.__wbindgen_export2, wasm.__wbindgen_export3); + const ptr0 = passStringToWasm0(name, wasm.__wbindgen_export, wasm.__wbindgen_export2); const len0 = WASM_VECTOR_LEN; - wasm.cstxgraph_updateNodeFlags(retptr, this.__wbg_ptr, ptr0, len0, add, remove, set_to); + wasm.schemas_loadPlugin(retptr, this.__wbg_ptr, ptr0, len0); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); - if (r2) { - throw takeObject(r1); + if (r1) { + throw takeObject(r0); } - return r0 !== 0; } finally { wasm.__wbindgen_add_to_stack_pointer(16); } } /** - * @returns {string} + * @param {string} node_type + * @param {any} schema + * @param {string | null} [value_field] */ - version() { - let deferred1_0; - let deferred1_1; + register(node_type, schema, value_field) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm.cstxgraph_version(retptr, this.__wbg_ptr); + const ptr0 = passStringToWasm0(node_type, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + var ptr1 = isLikeNone(value_field) ? 0 : passStringToWasm0(value_field, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + wasm.schemas_register(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(schema), ptr1, len1); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - deferred1_0 = r0; - deferred1_1 = r1; - return getStringFromWasm0(r0, r1); + if (r1) { + throw takeObject(r0); + } } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } } -if (Symbol.dispose) CSTXGraph.prototype[Symbol.dispose] = CSTXGraph.prototype.free; +if (Symbol.dispose) Schemas.prototype[Symbol.dispose] = Schemas.prototype.free; /** - * @param {string} source_type - * @param {Uint8Array} data * @returns {string} */ -export function cstxTransform(source_type, data) { - let deferred4_0; - let deferred4_1; +export function version() { + let deferred1_0; + let deferred1_1; try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - const ptr0 = passStringToWasm0(source_type, wasm.__wbindgen_export2, wasm.__wbindgen_export3); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passArray8ToWasm0(data, wasm.__wbindgen_export2); - const len1 = WASM_VECTOR_LEN; - wasm.cstxTransform(retptr, ptr0, len0, ptr1, len1); + wasm.version(retptr); var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); - var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); - var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); - var ptr3 = r0; - var len3 = r1; - if (r3) { - ptr3 = 0; len3 = 0; - throw takeObject(r2); - } - deferred4_0 = ptr3; - deferred4_1 = len3; - return getStringFromWasm0(ptr3, len3); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_export4(deferred4_0, deferred4_1, 1); + wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1); } } - -/** - * @returns {bigint} - */ -export function flagsAllMask() { - const ret = wasm.flagsAllMask(); - return BigInt.asUintN(64, ret); -} - -/** - * @returns {bigint} - */ -export function flagsDefaultExcludeMask() { - const ret = wasm.flagsDefaultExcludeMask(); - return BigInt.asUintN(64, ret); -} function __wbg_get_imports() { const import0 = { __proto__: null, @@ -900,12 +864,243 @@ function __wbg_get_imports() { const ret = Error(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }, + __wbg_Number_9a4e0ecb0fa16705: function(arg0) { + const ret = Number(getObject(arg0)); + return ret; + }, + __wbg_String_8564e559799eccda: function(arg0, arg1) { + const ret = String(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_bigint_get_as_i64_d968e41184ae354f: function(arg0, arg1) { + const v = getObject(arg1); + const ret = typeof(v) === 'bigint' ? v : undefined; + getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }, + __wbg___wbindgen_boolean_get_fa956cfa2d1bd751: function(arg0) { + const v = getObject(arg0); + const ret = typeof(v) === 'boolean' ? v : undefined; + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; + }, + __wbg___wbindgen_debug_string_c25d447a39f5578f: function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_in_aca499c5de7ff5e5: function(arg0, arg1) { + const ret = getObject(arg0) in getObject(arg1); + return ret; + }, + __wbg___wbindgen_is_bigint_2f76dc55065b4273: function(arg0) { + const ret = typeof(getObject(arg0)) === 'bigint'; + return ret; + }, + __wbg___wbindgen_is_function_1ff95bcc5517c252: function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; + }, + __wbg___wbindgen_is_null_ea9085d691f535d3: function(arg0) { + const ret = getObject(arg0) === null; + return ret; + }, + __wbg___wbindgen_is_object_a27215656b807791: function(arg0) { + const val = getObject(arg0); + const ret = typeof(val) === 'object' && val !== null; + return ret; + }, + __wbg___wbindgen_is_string_ea5e6cc2e4141dfe: function(arg0) { + const ret = typeof(getObject(arg0)) === 'string'; + return ret; + }, + __wbg___wbindgen_is_undefined_c05833b95a3cf397: function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }, + __wbg___wbindgen_jsval_eq_e659fcf7b0e32763: function(arg0, arg1) { + const ret = getObject(arg0) === getObject(arg1); + return ret; + }, + __wbg___wbindgen_jsval_loose_eq_db4c3b15f63fc170: function(arg0, arg1) { + const ret = getObject(arg0) == getObject(arg1); + return ret; + }, + __wbg___wbindgen_number_get_394265ed1e1b84ee: function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'number' ? obj : undefined; + getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }, + __wbg___wbindgen_string_get_b0ca35b86a603356: function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, __wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }, + __wbg_call_8a2dd23819f8a60a: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments); }, + __wbg_done_89b2b13e91a60321: function(arg0) { + const ret = getObject(arg0).done; + return ret; + }, + __wbg_entries_015dc610cd81ede0: function(arg0) { + const ret = Object.entries(getObject(arg0)); + return addHeapObject(ret); + }, __wbg_getRandomValues_3f44b700395062e5: function() { return handleError(function (arg0, arg1) { globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1)); }, arguments); }, + __wbg_get_507a50627bffa49b: function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }, + __wbg_get_c7eb1f358a7654df: function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments); }, + __wbg_get_unchecked_6e0ad6d2a41b06f6: function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }, + __wbg_get_with_ref_key_6412cf3094599694: function(arg0, arg1) { + const ret = getObject(arg0)[getObject(arg1)]; + return addHeapObject(ret); + }, + __wbg_instanceof_ArrayBuffer_4480b9e0068a8adb: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof ArrayBuffer; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Map_e5b5e3db98422fcc: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Map; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Uint8Array_309b927aaf7a3fc7: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_isArray_0677c962b281d01a: function(arg0) { + const ret = Array.isArray(getObject(arg0)); + return ret; + }, + __wbg_isSafeInteger_04f36e4056f1b851: function(arg0) { + const ret = Number.isSafeInteger(getObject(arg0)); + return ret; + }, + __wbg_iterator_6f722e4a93058b71: function() { + const ret = Symbol.iterator; + return addHeapObject(ret); + }, + __wbg_length_1f0964f4a5e2c6d8: function(arg0) { + const ret = getObject(arg0).length; + return ret; + }, + __wbg_length_370319915dc99107: function(arg0) { + const ret = getObject(arg0).length; + return ret; + }, + __wbg_new_32b398fb48b6d94a: function() { + const ret = new Array(); + return addHeapObject(ret); + }, + __wbg_new_7796ffc7ed656783: function() { + const ret = new Map(); + return addHeapObject(ret); + }, + __wbg_new_b667d279fd5aa943: function(arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }, + __wbg_new_cd45aabdf6073e84: function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }, + __wbg_new_da52cf8fe3429cb2: function() { + const ret = new Object(); + return addHeapObject(ret); + }, + __wbg_next_6dbf2c0ac8cde20f: function(arg0) { + const ret = getObject(arg0).next; + return addHeapObject(ret); + }, + __wbg_next_71f2aa1cb3d1e37e: function() { return handleError(function (arg0) { + const ret = getObject(arg0).next(); + return addHeapObject(ret); + }, arguments); }, + __wbg_prototypesetcall_4770620bbe4688a0: function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2)); + }, + __wbg_set_575dd786d51585f8: function(arg0, arg1, arg2) { + const ret = getObject(arg0).set(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, + __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) { + getObject(arg0)[takeObject(arg1)] = takeObject(arg2); + }, + __wbg_set_8535240470bf2500: function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments); }, + __wbg_set_8a16b38e4805b298: function(arg0, arg1, arg2) { + getObject(arg0)[arg1 >>> 0] = takeObject(arg2); + }, + __wbg_value_a5d5488a9589444a: function(arg0) { + const ret = getObject(arg0).value; + return addHeapObject(ret); + }, + __wbindgen_cast_0000000000000001: function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return addHeapObject(ret); + }, + __wbindgen_cast_0000000000000002: function(arg0) { + // Cast intrinsic for `I64 -> Externref`. + const ret = arg0; + return addHeapObject(ret); + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_cast_0000000000000004: function(arg0) { + // Cast intrinsic for `U64 -> Externref`. + const ret = BigInt.asUintN(64, arg0); + return addHeapObject(ret); + }, + __wbindgen_object_clone_ref: function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }, __wbindgen_object_drop_ref: function(arg0) { takeObject(arg0); }, @@ -916,9 +1111,24 @@ function __wbg_get_imports() { }; } -const CSTXGraphFinalization = (typeof FinalizationRegistry === 'undefined') +const CSTXFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_cstx_free(ptr, 1)); +const EdgeCursorFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_edgecursor_free(ptr, 1)); +const GraphFinalization = (typeof FinalizationRegistry === 'undefined') ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(ptr => wasm.__wbg_cstxgraph_free(ptr, 1)); + : new FinalizationRegistry(ptr => wasm.__wbg_graph_free(ptr, 1)); +const NodeCursorFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_nodecursor_free(ptr, 1)); +const RepositoryFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_repository_free(ptr, 1)); +const SchemasFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_schemas_free(ptr, 1)); function addHeapObject(obj) { if (heap_next === heap.length) heap.push(heap.length + 1); @@ -929,6 +1139,71 @@ function addHeapObject(obj) { return idx; } +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + function dropObject(idx) { if (idx < 1028) return; heap[idx] = heap_next; @@ -966,7 +1241,7 @@ function handleError(f, args) { try { return f.apply(this, args); } catch (e) { - wasm.__wbindgen_export(addHeapObject(e)); + wasm.__wbindgen_export3(addHeapObject(e)); } } diff --git a/ts/wasm/cstx_wasm_bg.wasm b/ts/wasm/cstx_wasm_bg.wasm index 4dd48e3..cc5a69f 100644 Binary files a/ts/wasm/cstx_wasm_bg.wasm and b/ts/wasm/cstx_wasm_bg.wasm differ diff --git a/ts/wasm/cstx_wasm_bg.wasm.d.ts b/ts/wasm/cstx_wasm_bg.wasm.d.ts index fb611e7..1981ba4 100644 --- a/ts/wasm/cstx_wasm_bg.wasm.d.ts +++ b/ts/wasm/cstx_wasm_bg.wasm.d.ts @@ -1,51 +1,65 @@ /* tslint:disable */ /* eslint-disable */ export const memory: WebAssembly.Memory; -export const __wbg_cstxgraph_free: (a: number, b: number) => void; -export const cstxTransform: (a: number, b: number, c: number, d: number, e: number) => void; -export const cstxgraph_addEdge: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void; -export const cstxgraph_addEdgesBatch: (a: number, b: number, c: number, d: number) => void; -export const cstxgraph_addJoinRule: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number) => void; -export const cstxgraph_addNode: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: bigint) => void; -export const cstxgraph_addNodesBatch: (a: number, b: number, c: number, d: number) => void; -export const cstxgraph_allNodesJson: (a: number, b: number) => void; -export const cstxgraph_availablePlugins: (a: number, b: number) => void; -export const cstxgraph_bfs: (a: number, b: number, c: number, d: number, e: number, f: number) => void; -export const cstxgraph_containsNode: (a: number, b: number, c: number) => number; -export const cstxgraph_degree: (a: number, b: number, c: number, d: number, e: number) => number; -export const cstxgraph_edgeCount: (a: number) => number; -export const cstxgraph_edgesFiltered: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; -export const cstxgraph_edgesJson: (a: number, b: number, c: number, d: number) => void; -export const cstxgraph_exportSnapshotJson: (a: number, b: number) => void; -export const cstxgraph_hasNativeArtifact: (a: number, b: number, c: number) => number; -export const cstxgraph_ingestJsonl: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void; -export const cstxgraph_ingestNative: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; -export const cstxgraph_isPathExpression: (a: number, b: number, c: number) => number; -export const cstxgraph_link: (a: number, b: number, c: number, d: number, e: number, f: number) => void; -export const cstxgraph_loadAllPlugins: (a: number) => number; -export const cstxgraph_loadPlugin: (a: number, b: number, c: number, d: number) => void; -export const cstxgraph_neighborIds: (a: number, b: number, c: number, d: number, e: number, f: number) => void; -export const cstxgraph_neighborsJson: (a: number, b: number, c: number, d: number) => void; -export const cstxgraph_new: () => number; -export const cstxgraph_nodeCount: (a: number) => number; -export const cstxgraph_nodeIds: (a: number, b: number) => void; -export const cstxgraph_nodePayload: (a: number, b: number, c: number, d: number) => void; -export const cstxgraph_nodeTypes: (a: number, b: number) => void; -export const cstxgraph_nodesJson: (a: number, b: number, c: number, d: number) => void; -export const cstxgraph_queryDsl: (a: number, b: number, c: number, d: number, e: number, f: number) => void; -export const cstxgraph_queryNodeIds: (a: number, b: number, c: number, d: number, e: number, f: number) => void; -export const cstxgraph_registerSchema: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; -export const cstxgraph_shortestPaths: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; -export const cstxgraph_stats: (a: number, b: number) => void; -export const cstxgraph_subgraphNodeIds: (a: number, b: number, c: number, d: number, e: number) => void; -export const cstxgraph_supportedArtifacts: (a: number, b: number) => void; -export const cstxgraph_toJson: (a: number, b: number) => void; -export const cstxgraph_updateNodeFlags: (a: number, b: number, c: number, d: number, e: bigint, f: bigint, g: bigint) => void; -export const cstxgraph_version: (a: number, b: number) => void; -export const flagsAllMask: () => bigint; -export const flagsDefaultExcludeMask: () => bigint; -export const __wbindgen_export: (a: number) => void; +export const __wbg_cstx_free: (a: number, b: number) => void; +export const __wbg_edgecursor_free: (a: number, b: number) => void; +export const __wbg_graph_free: (a: number, b: number) => void; +export const __wbg_nodecursor_free: (a: number, b: number) => void; +export const cstx_close: (a: number) => void; +export const cstx_closed: (a: number) => number; +export const cstx_graph: (a: number) => number; +export const cstx_new: (a: number, b: number) => void; +export const edgecursor_close: (a: number) => void; +export const edgecursor_closed: (a: number) => number; +export const edgecursor_next: (a: number, b: number) => void; +export const graph_addEdge: (a: number, b: number, c: number) => void; +export const graph_addEdges: (a: number, b: number, c: number) => void; +export const graph_addEdgesJson: (a: number, b: number, c: number, d: number) => void; +export const graph_addNode: (a: number, b: number, c: number) => void; +export const graph_addNodes: (a: number, b: number, c: number) => void; +export const graph_addNodesJson: (a: number, b: number, c: number, d: number) => void; +export const graph_contains: (a: number, b: number, c: number, d: number) => void; +export const graph_edgeCount: (a: number, b: number) => void; +export const graph_edges: (a: number, b: number, c: number) => void; +export const graph_ingest: (a: number, b: number, c: number, d: number, e: number, f: number) => void; +export const graph_neighbors: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; +export const graph_node: (a: number, b: number, c: number, d: number) => void; +export const graph_nodeCount: (a: number, b: number) => void; +export const graph_nodes: (a: number, b: number, c: number) => void; +export const graph_query: (a: number, b: number, c: number, d: number, e: number) => void; +export const graph_stats: (a: number, b: number) => void; +export const nodecursor_close: (a: number) => void; +export const nodecursor_closed: (a: number) => number; +export const nodecursor_next: (a: number, b: number) => void; +export const repository_commit: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; +export const repository_diff: (a: number, b: number, c: number, d: number, e: number, f: number) => void; +export const repository_dump: (a: number, b: number, c: number, d: number) => void; +export const repository_head: (a: number, b: number, c: number, d: number) => void; +export const repository_load: (a: number, b: number, c: number, d: number, e: number, f: number) => void; +export const repository_refs: (a: number, b: number) => void; +export const repository_snapshotFingerprint: (a: number, b: number) => void; +export const schemas_availablePlugins: (a: number, b: number) => void; +export const schemas_contains: (a: number, b: number, c: number, d: number) => void; +export const schemas_get: (a: number, b: number, c: number, d: number) => void; +export const schemas_list: (a: number, b: number) => void; +export const schemas_loadAllPlugins: (a: number, b: number) => void; +export const schemas_loadPlugin: (a: number, b: number, c: number, d: number) => void; +export const schemas_register: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; +export const version: (a: number) => void; +export const rust_zstd_wasm_shim_calloc: (a: number, b: number) => number; +export const rust_zstd_wasm_shim_free: (a: number) => void; +export const rust_zstd_wasm_shim_malloc: (a: number) => number; +export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number; +export const rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number; +export const rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number; +export const rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number; +export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void; +export const cstx_repository: (a: number) => number; +export const cstx_schemas: (a: number) => number; +export const __wbg_repository_free: (a: number, b: number) => void; +export const __wbg_schemas_free: (a: number, b: number) => void; +export const __wbindgen_export: (a: number, b: number) => number; +export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_export3: (a: number) => void; export const __wbindgen_add_to_stack_pointer: (a: number) => number; -export const __wbindgen_export2: (a: number, b: number) => number; -export const __wbindgen_export3: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_export4: (a: number, b: number, c: number) => void;