Skip to content

API Stability Policy

This document defines the public API contract for the pgokf extension: what callers may depend on, how it is versioned, how it changes, and what is explicitly not part of the contract. It is the reference that CHANGELOG.md classifies changes against and that the release checklist enforces.

What "public API" means here

The public API is the surface an application, dashboard, or migration can bind to and expect to keep working across compatible releases. For pgokf that surface is deliberately narrow and fully enumerated below. Everything reachable in the database that is not on these lists is internal and may change without notice.

Every public object is self-describing: it carries a SQL COMMENT ON, so \df+, \dT+, \d+, and obj_description() document the contract from inside the database. Complete comment coverage is a release gate (see Documentation coverage).

The stable surface

Functions (69)

The installed extension defines one function more than this table: the internal pgokf.bm25_hits helper the optional BM25 backend calls. It is not part of the stable surface, carries no compatibility promise, and may change or disappear in any release.

Function Role required Purpose
pgokf.register_bundle(text, text, jsonb) pgokf_writer Register and sync an OKF bundle root from a filesystem path
pgokf.register_bundle_content(text, text[], bytea[], jsonb) pgokf_writer Register or resync a bundle from in-memory (path, bytes) content - the mountless object-store ingestion path (no filesystem, no network I/O in the extension)
pgokf.refresh_bundle(bigint) pgokf_writer Incrementally re-sync a filesystem-sourced bundle (content bundles are re-synced via register_bundle_content)
pgokf.unregister_bundle(bigint) pgokf_writer Remove a bundle (rows cascade)
pgokf.set_bundle_enabled(bigint, boolean) pgokf_writer Enable/disable a bundle (hides it from search and graph without deleting rows; reversible)
pgokf.retire_bundle(bigint) pgokf_writer Soft-retire a bundle (hides it, reversibly) into a purge window
pgokf.unretire_bundle(bigint) pgokf_writer Reverse a retirement (clears retired_at)
pgokf.purge_retired(interval) pgokf_admin Hard-delete bundles retired longer than the given window
pgokf.list_bundles() pgokf_reader List registered bundles
pgokf.bundle_info(bigint) pgokf_reader Info for one registered bundle
pgokf.duplicate_concepts(bigint, integer) pgokf_reader Concepts sharing a BLAKE3 file_hash across bundles (dedup)
pgokf.concept_search(text, bigint, integer, text, text[], text, text, jsonb) pgokf_reader Ranked full-text search with optional filters and an optional trailing after_cursor jsonb for stable keyset pagination (trailing args default NULL; the historical 3-arg call still resolves)
pgokf.search_facets(text, bigint, text, text, text[], text, text) pgokf_reader Faceted result counts for a query (by type/tag/bundle/status/trust_tier)
pgokf.search_index_status() pgokf_reader Search-index availability + coverage report (native/bm25/pgvector) as jsonb
pgokf.find_similar(text, bigint, integer) pgokf_reader Content more-like-this for a seed concept (distinct from the link graph)
pgokf.concept_search_semantic(real[], bigint, integer, text[]) pgokf_reader Vector nearest-neighbor search by a caller-supplied query embedding (requires pgvector); the optional trailing concept_types filters by type membership inside the ranked query, before candidate truncation
pgokf.concept_search_hybrid(text, real[], bigint, integer, text[]) pgokf_reader RRF fusion of lexical + semantic (degrades to lexical when pgvector absent); the optional trailing concept_types constrains both ranked inputs before either is truncated
pgokf.set_concept_embedding(bigint, text, real[]) pgokf_writer Store a caller-computed embedding for a concept (no bundled model)
pgokf.rebuild_embedding_index() pgokf_admin (Re)build the optional pgvector HNSW index; a no-op with a notice when pgvector is not installed
pgokf.concept_neighbors(text, integer, bigint) pgokf_reader Walk the resolved link graph
pgokf.concept_history(bigint, text, integer) pgokf_reader Version timeline of a concept (opt-in track_history)
pgokf.concept_as_of(bigint, text, timestamptz) pgokf_reader The concept version valid at a point in time
pgokf.set_config(text, jsonb) pgokf_admin Set a durable configuration key
pgokf.reset_config(text) pgokf_admin Reset one/all configuration keys
pgokf.get_config() pgokf_reader Effective configuration as jsonb
pgokf.list_sync_log(bigint, integer) pgokf_reader Sync/audit history rows (from the admin-only pgokf_private.sync_log)
pgokf.list_sync_changes(bigint, integer) pgokf_reader Per-concept change manifest (added/updated/removed) for one sync
pgokf.list_access_log(bigint, integer) pgokf_admin Exfiltration/access audit rows (exports + get_concept_source)
pgokf.list_bundle_log(bigint, text, integer) pgokf_reader Reserved OKF log.md activity-log entries, projected per directory
pgokf.catalog_stats() pgokf_reader Per-bundle counts, sync recency, and staleness for operators
pgokf.health() pgokf_reader Liveness/readiness document as jsonb (counts, backend, in_recovery)
pgokf.stale_concepts(bigint, timestamptz) pgokf_reader Concepts past their OKF stale_after
pgokf.get_concept_source(bigint, text) pgokf_reader Return one concept's stored source bytes as bytea (no filesystem write)
pgokf.get_skill(bigint, text) pgokf_reader Return one Agent Skills package (skill_result): metadata, the exact SKILL.md, and its resource listing; audited
pgokf.get_script(bigint, text) pgokf_reader Return one package script's exact bytes and typed metadata (script_result); audited
pgokf.get_reference(bigint, text, boolean) pgokf_reader Return one package reference or asset (reference_result), bytes included unless include_bytes is false; audited when bytes are returned
pgokf.export_parquet(bigint, text) pgokf_admin Export a bundle projection to Parquet files
pgokf.export_sources(bigint, text) pgokf_admin Reconstruct a bundle's stored source files on disk, hash-verified
pgokf.rebuild_search_index() pgokf_admin (Re)build the optional BM25 index for the provider bm25_provider resolves to (pg_textsearch or pg_search); a no-op with a notice when none is installed
pgokf.schedule_refresh(bigint, text) pgokf_admin Schedule periodic refresh_bundle via pg_cron (raises when pg_cron is absent)
pgokf.unschedule_refresh(bigint) pgokf_admin Remove a bundle's scheduled refresh
pgokf.version() pgokf_reader Loaded shared-library version
pgokf.tenant_required() any role with USAGE on pgokf Whether the require_tenant policy is on (consulted by every RLS policy)
pgokf.mcp_token_bearer(text) pgokf_reader The name, role, and tenant of the MCP bearer token with this SHA-256 digest, or no row (SECURITY DEFINER over pgokf_web.mcp_tokens, which no reader may see)
pgokf.register_bundle_content_with_context(text, text[], bytea[], jsonb, jsonb) pgokf_writer register_bundle_content with an explicit change-provenance context (when context is NULL, the session pgokf.sync_context GUC supplies the same provenance)
pgokf.capabilities() pgokf_reader The capability names and interface versions this release implements, as jsonb
pgokf.concept_search_fresh(text, bigint, integer, text, text, text[], text, text, jsonb) pgokf_reader Freshness-aware variant of concept_search: a freshness filter (any/fresh/stale) plus per-hit effective-freshness and embedding-provenance annotations
pgokf.register_freshness_dependency(text, bigint, text, bigint, text, text, text, text) pgokf_writer Map a source-bundle selector onto a target bundle/scope so source changes mark matched targets stale
pgokf.disable_freshness_dependency(bigint) pgokf_writer Disable a registered freshness dependency
pgokf.remove_freshness_dependency(bigint) pgokf_writer Remove a registered freshness dependency
pgokf.mark_stale(bigint, text[], text, text) pgokf_writer Mark a bundle stale with reason codes and the observed source revision
pgokf.mark_reconciling(bigint, text) pgokf_writer Mark a bundle as reconciling and claim the standing dependency invalidation epoch (returns the attempt's claim token)
pgokf.mark_blocked(bigint, text[], text) pgokf_writer Mark a bundle blocked with reason codes
pgokf.mark_fresh(bigint, bigint, bigint, text, text, jsonb, text) pgokf_writer Compare-and-set reconciliation completion: refuses (returns false) when the catalog generation, observed source revision, or the attempt's claim token moved meanwhile
pgokf.mark_scope_stale(bigint, text, text, text[], text) pgokf_writer Mark one scope (concept/path/group) of a bundle stale
pgokf.clear_freshness_scope(bigint, text, text) pgokf_writer Clear a scoped freshness override
pgokf.list_freshness_dependencies(integer) pgokf_admin Inspect the freshness-dependency registry
pgokf.repair_bundle_freshness(bigint, text, text[]) pgokf_admin Repair a bundle's freshness state directly
pgokf.issue_publication_fence(bigint, text, bigint, bigint, text, integer) pgokf_writer Compare-and-set the producer's publication fence slot under the bundle advisory lock; hands out the next fencing token
pgokf.release_publication_fence(bigint, text, bigint) pgokf_writer Release a live publication fence token
pgokf.claim_catalog_change_events(text, integer, integer) pgokf_dispatcher Claim pending or expired-claim catalog-change outbox events oldest-first (FOR UPDATE SKIP LOCKED) with a lease
pgokf.ack_catalog_change_event(bigint, text, text) pgokf_dispatcher Acknowledge a claimed outbox event after durable producer acceptance (idempotent, producer-bound)
pgokf.list_catalog_change_events(bigint, integer) pgokf_admin Inspect the catalog-change outbox, newest first
pgokf.replace_relationships(text, bigint, bigint, bigint, bigint, jsonb) pgokf_writer Publish a source bundle's complete typed-relationship set, bound to the producer's live publication fence
pgokf.concept_relationship_neighbors(bigint, text, integer, text, text[], integer) pgokf_reader Walk the current typed-relationship set cycle-safely (direction, optional type filter, hop and result ceilings)
pgokf.set_concept_embedding_cas(bigint, text, real[], text, text, text, text) pgokf_writer Compare-and-set embedding write: stores only when the expected file hash still matches, so a superseded attempt cannot overwrite newer content
pgokf.list_scheduled_refreshes() pgokf_reader List the scheduled bundle refreshes the extension manages, as (bundle_id, schedule) rows (SECURITY DEFINER over cron.job, which row-restricts an ordinary login to its own jobs while schedule_refresh registers them under the extension owner; tenant-confined)
pgokf.registry_set_status(uuid, text) pgokf_admin Pause/resume one registered repository of the external repository-registry producer service (ast_graph.repository_registry.status, active/paused; runtime-only coupling, curated 22023 when the producer schema is absent; tenant-confined: a cross-tenant id earns the same 22023 as an unknown one)
pgokf.registry_set_poll_interval(uuid, integer) pgokf_admin Set one registered repository's poll interval in seconds (5-86400) on the same external registry (same runtime-only coupling, 22023 behavior, and tenant confinement)

Every row from pgokf.register_bundle_content_with_context down became stable in the 0.3.0 development cycle; the rows above it predate that cycle.

The function name, schema, argument types, argument order, and result shape are all part of the contract. Default values that let callers omit trailing arguments (name/options on register_bundle, bundle_id/limit on search and neighbors) are contractual too: an existing call that omits them keeps working.

Composite types (24)

pgokf.bundle_sync_result, pgokf.concept_search_result, pgokf.concept_neighbor, pgokf.bundle_info, pgokf.export_result, pgokf.sync_log_entry, pgokf.catalog_stat, pgokf.stale_concept, pgokf.sync_change, pgokf.access_log_entry, pgokf.duplicate_group, pgokf.search_facet, pgokf.bundle_log_entry, pgokf.concept_version, pgokf.skill_result, pgokf.script_result, pgokf.reference_result, pgokf.freshness_dependency_info, pgokf.publication_fence_info, pgokf.claimed_change_event, pgokf.catalog_change_event_info, pgokf.concept_search_fresh_result, pgokf.relationship_publication_info, pgokf.relationship_neighbor (the last seven became stable in the 0.3.0 development cycle).

The set of columns, their names, and their types are stable. New columns are not added to an existing composite type in a compatible release, because SELECT * and positional row expansion would break; a new field ships as a new type or a new function instead.

Tables (14 public + 4 documented-internal)

Public, SELECT-able by pgokf_reader: pgokf.bundles, pgokf.concepts, pgokf.concept_metadata, pgokf.links, pgokf.concept_provenance, pgokf.concept_verification, pgokf.concept_provenance_source, pgokf.concept_source, pgokf.concept_embedding, pgokf.bundle_log, pgokf.concept_history, pgokf.skills, pgokf.scripts, pgokf.reference_documents.

These are a read projection. Callers may SELECT from them and depend on existing column names and types; the columns listed in docs/sql-api.md are the contract. Direct INSERT/UPDATE/ DELETE is not part of the API - mutation goes exclusively through the SECURITY DEFINER sync functions, and the tables carry no write grant to pgokf_reader. New columns may be added to these projection tables in a compatible release; code that pins to named columns (never SELECT * into a fixed row type) is forward-compatible.

pgokf_private.config, pgokf_private.sync_log, pgokf_private.sync_log_change, and pgokf_private.access_log are listed here only because they are catalog tables the documentation gate covers (the fourteen public tables plus these four private ones). They are internal state, not API - read the sync history through pgokf.list_sync_log and configuration through pgokf.get_config; see The private surface.

Roles (4)

pgokf_reader < pgokf_writer < pgokf_admin. Their names and the privilege boundaries between them are stable: readers may search and read configuration; writers may additionally register/refresh/unregister bundles (the intended tier for an ingestion pipeline); admins may additionally manage configuration and run the file-writing exports. Each tier inherits the one below (pgokf_admin → pgokf_writer → pgokf_reader). These are cluster-wide roles and survive DROP EXTENSION.

Since the 0.3.0 development cycle a fourth role, pgokf_dispatcher, stands deliberately outside the ladder: it inherits nothing and is inherited by nothing. It is the sole claim/acknowledge role for the catalog-change outbox (pgokf.claim_catalog_change_events, pgokf.ack_catalog_change_event) and holds no search or ingestion rights; admins inspect the outbox through pgokf.list_catalog_change_events.

GUC names (9)

pgokf.max_file_bytes, pgokf.max_bundle_files, pgokf.max_bundle_bytes, pgokf.max_frontmatter_bytes, pgokf.max_graph_hops, pgokf.max_relationship_rows, pgokf.log_level, and pgokf.tenant (the USERSET multi-tenant policy selector; empty by default, which preserves the pre-multi-tenancy see-all behavior), plus - since the 0.3.0 development cycle - pgokf.sync_context (the USERSET change-provenance context a producer session sets before a sync; the committed catalog-change event carries it). The names and their meaning are stable; default values are tuning knobs and may be adjusted in a minor release when a change is safe and documented.

Semantic versioning policy

pgokf follows Semantic Versioning 2.0.0. Given MAJOR.MINOR.PATCH, and treating the stable surface above as the contract:

  • MAJOR - a breaking change to the stable surface: removing or renaming a function/type/table/role/GUC, changing a function's argument types or order, removing a default that callers relied on, changing a result-type column set, narrowing a privilege in a way that breaks existing callers, or changing documented behavior in an incompatible way.
  • MINOR - backward-compatible additions: a new function, a new composite type, a new projection column, a new GUC, a new optional trailing argument with a default, or new forward-compatible behavior.
  • PATCH - backward-compatible fixes: bug fixes, performance improvements, documentation, and internal refactors that leave the stable surface unchanged.

Pre-1.0 caveat

While the version is 0.x, the surface is not yet frozen. Per SemVer, 0.y.z makes no compatibility promise across y bumps. In practice this project already treats the enumerated surface as stable and documents every change in the changelog, but until a 1.0.0 tag is cut a 0.MINOR bump may carry a breaking change when it is called out explicitly in the changelog under a Changed or Removed heading. Reaching 1.0.0 is the point at which the MAJOR/MINOR/PATCH rules above become binding guarantees, and it is a deliberate human release decision - not an automated version bump.

Development point versions

Between releases the working line is point-versioned: the cycle toward 0.3.0 ships as 0.3.0-dev1, 0.3.0-dev2, and so on, with pgokf.control's default_version and the workspace crate version moving together on every step. The reason is operational: deployments track development builds, and a flat 0.3.0-dev gives SQL objects added mid-cycle no ALTER EXTENSION pgokf UPDATE target - they would have to be applied by hand, outside the extension's receipts. With point versions, every SQL change rides its own pgokf--0.3.0-devN--0.3.0-dev<N+1>.sql script through the normal update machinery.

The naming needs no special dispensation from PostgreSQL: extension version names are opaque strings to the update machinery. ALTER EXTENSION ... UPDATE finds its path by exact pgokf--<from>--<to>.sql file-name matching (a graph walk over the shipped scripts, with the control file's default_version - itself a string, never a comparison - as the goal of a bare UPDATE), so 0.3.0-dev -> 0.3.0-dev1 resolves like any other edge once the script file exists. pgrx accepts the suffix in both the control file and the Cargo package version (a SemVer pre-release identifier), and the api_stability suite locks the convention's shape (MAJOR.MINOR.PATCH-devN, N starting at 1).

Forward discipline, test-enforced. Any change that adds or alters a shipped SQL object must, in the same change: bump the dev point version (control file and workspace Cargo.toml together) and ship the devN -> devN+1 upgrade script whose target equals the new default_version. tests/api_stability.rs fails the build otherwise: it requires the control version to match the crate version, the script chain to walk from 0.1.0 to the default_version, and the default_version to be the chain's unique terminal node - no script may name it as a source, so a step shipped without the default bump (a dev1 -> dev2 script while the default stays dev1) fails the suite, because bare ALTER EXTENSION pgokf UPDATE would still target the old terminal and never run it. The check walks the pgokf--<from>--<to>.sql edges as a graph and never compares version strings; a deliberate historical branch, should one ever ship, must be listed explicitly in PERMITTED_BRANCH_TERMINALS. Where a mid-cycle object may already exist in the field (deployed from an earlier or later point of the cycle, or applied by hand), the step's statements must be idempotent - CREATE OR REPLACE, guarded DO blocks - and must adopt hand-applied copies into extension membership.

Collapse at finalization. When the cycle closes, default_version becomes the clean 0.3.1, the install script regenerates under that name and is committed (pgrx's entity ordering is unstable across invocations, so the committed snapshot is validated by the catalog it produces, not by byte-equality with a regeneration), and the preserved pgokf--0.3.0-dev3--0.3.0.sql receipt followed by pgokf--0.3.0--0.3.1.sql (a no-op beyond the standing register_dump_relations() call) lets point-versioned deployments reach the release through the same machinery. The upgrade chain from the previous tag then runs ... -> 0.2.0 -> 0.3.0-dev -> 0.3.0-dev1 -> ... -> 0.3.0 -> 0.3.1; the fresh-install-vs-upgrade parity harness proves the two routes converge object-by-object, and that every deployed point version reaches the release through a bare ALTER EXTENSION pgokf UPDATE.

Deprecation process

Nothing on the stable surface is removed abruptly. The process is:

  1. Announce. Mark the object deprecated in the changelog under Deprecated, and update its SQL COMMENT ON to say so and to name the replacement.
  2. Coexist. The deprecated object keeps working for at least one full MINOR release (post-1.0) alongside its replacement, so callers can migrate incrementally.
  3. Remove. Removal happens only in a subsequent MAJOR release, listed under Removed in the changelog, with the migration path repeated there.

Extension upgrade scripts (pgokf--<from>--<to>.sql) carry any data migration a deprecation requires and must remain forward-compatible - see the upgrade mechanism - so ALTER EXTENSION pgokf UPDATE never loses catalog data.

The private surface (NOT API)

The following are internal implementation detail. They may change, move, or disappear in any release, and callers must not depend on them:

  • The entire pgokf_private schema, including pgokf_private.config. It is administrator-only state managed exclusively through pgokf.set_config / pgokf.reset_config / pgokf.get_config. Read and write it only through those functions.
  • The internal pgokf.* state tables behind the 0.3.0-cycle producer surface - pgokf.bundle_freshness, pgokf.concept_freshness, pgokf.freshness_dependency, pgokf.publication_fence, pgokf.catalog_change_event, pgokf.relationship_publication, and pgokf.relationship. They carry no grant to any reader role; reach the data through the documented views (pgokf.effective_freshness, pgokf.current_relationships) and functions.
  • Indexes, constraints, and trigger internals on the projection tables. Their existence and names are not contractual; query behavior is.
  • The SECURITY DEFINER wiring, search_path pinning, and grant details beyond the documented reader/writer/admin boundaries.
  • Rust module layout, the sync engine, and the parser crates. Only the SQL surface is public.
  • The exact text of COMMENT ON strings and error message wording. SQLSTATE codes raised by public functions (e.g. 23505 on duplicate registration, 22023 on invalid configuration, unknown-bundle errors) are documented behavior; the human-readable message text is not.

Documentation coverage is part of the contract

Every public object must carry a COMMENT ON. This is enforced two ways:

  • At build time - crates/extension/tests/api_stability.rs reads the extension SQL source and fails cargo test if any enumerated function, type, table, or role lacks a COMMENT ON, and if the count of public functions drifts from the locked surface.
  • At release time - the release checklist runs an obj_description / shobj_description coverage query against a freshly installed extension and blocks the release if any public object is uncommented.

Adding a public object therefore requires, in the same change: the object, its COMMENT ON, an entry in the enumerated contract test, a changelog entry, and (if it changes stored data) an upgrade script.

Development-function adoption security policy

The dev→dev1 edge repairs the exact signatures pgokf.list_scheduled_refreshes(), pgokf.registry_set_status(uuid,text) and pgokf.registry_set_poll_interval(uuid,integer). Missing functions are created. Existing functions, including pgokf members, must be owned by the extension owner and have exactly two explicit EXECUTE grants: to that owner and to pgokf_reader for the listing function, or pgokf_admin for each writer. Both grants must be issued by the owner without grant options. ACL ordering is irrelevant. Unexpected owners, additional grants (including PUBLIC), missing grants, different grantors and grant options refuse the entire upgrade with a security-metadata diagnostic. Custom grants are never silently revoked; the operator must review them and deliberately restore this posture before retrying. A compatible body may be replaced when this security metadata is canonical. Incompatible return/argument definitions or membership in another extension still refuse atomically. The live PG18 harness checks catalog and version rollback, including ownership, ACLs, initial privileges, body and membership. This edge can inspect only installations that traverse dev→dev1; it does not repair already-upgraded installations retroactively.

The immutable v0.3.0 tag is an unpublished candidate superseded by 0.3.1: its tag CI demanded unavailable GA PG19 artifacts and found a Linux strict-Clippy defect. The preserved graph includes 0.3.0-dev3 → 0.3.0 → 0.3.1, with no outgoing edge from 0.3.1. Candidate installations can upgrade additively; the candidate was not a published release or a production deployment.