Skip to contents

Knowledge change control is easier to reason about when there is one answer to “what was accepted?” In graft, immutable record revisions are that answer. Current records, search data, graph relationships, and the OKF working tree are derived views that can be rebuilt from the accepted ledger.

Every mutation follows the same sequence:

candidate records -> read-only plan -> review -> atomic commit -> revision history

This separation makes the approval boundary visible and prevents validation, OKF import, or convenience functions from becoming alternate write paths.

The objects at the boundary

graft uses S7 selectively where an object carries durable invariants:

  • GraftSchema owns a validated compiled domain contract and its digests.
  • GraftProvenance identifies the producer event and replay boundary.
  • GraftCommitPlan carries the candidate change set and commit preconditions.
  • GraftStore owns store identity and private connection state.

Domain records remain ordinary data frames. Retrieval results remain ordinary data frames or lists. graft does not create an S7 class for every contract class.

library(graft)

schema <- graft_schema(system.file(
  "extdata",
  "team-directory.data-dict.json",
  package = "graft",
  mustWork = TRUE
))
store <- graft_open(schema, ":memory:", okf = "disabled")

Build a reviewable plan

Provenance and records are separate inputs so the candidate’s origin cannot be confused with its domain content.

provenance <- graft_provenance(
  producer = "directory-import",
  run_id = "run-42",
  idempotency_key = "directory-2026-08-03"
)

records <- list(
  organization = data.frame(
    id = "org:daily-planet",
    name = "Daily Planet"
  ),
  person = data.frame(
    id = "person:lois-lane",
    full_name = "Lois Lane",
    job_title = "Reporter"
  ),
  employment = data.frame(
    id = "employment:lois-lane:daily-planet",
    person_id = "person:lois-lane",
    organization_id = "org:daily-planet"
  )
)

plan <- graft_plan(store, records, provenance)

Planning is deterministic for the same contract, store snapshot, records, and provenance. It performs the work needed for an informed review:

  • normalize values and validate the compiled contract shapes;
  • resolve stable record identity;
  • validate references across the complete candidate set;
  • compare candidates with current accepted heads;
  • classify each candidate as an insert, update, or match;
  • collect validation issues with source-row coordinates; and
  • bind the plan to the store, schema, expected heads, and a plan digest.

No accepted record or batch metadata is written during this step.

plan@valid
plan@changes
plan@issues

A reviewer should examine the proposed dispositions and changed fields, not only plan@valid. Valid means that the plan can be committed; it does not mean that the proposed knowledge is desirable.

Commit with optimistic preconditions

if (plan@valid) {
  result <- graft_commit(store, plan)
}

Immediately before mutation, graft rechecks the plan digest, store identity, store format, active schema digest, idempotency state, write capability, and expected record heads. If another accepted change made the plan stale, the commit fails and the caller must plan again against the new state.

The accepted changes, provenance, identity decisions, and observations commit together. A failure before transaction completion leaves no partially accepted change.

Recommitting the exact reviewed plan with its committed producer and idempotency key returns the original result without adding accepted metadata. Reusing that key for a different plan fails. This makes retry behavior explicit at the workflow boundary.

Use the convenience path deliberately

graft_ingest() plans and immediately commits when the plan is valid:

convenience_result <- graft_ingest(
  store,
  list(
    organization = data.frame(
      id = "org:metropolis-university",
      name = "Metropolis University"
    )
  ),
  graft_provenance(
    producer = "directory-import",
    idempotency_key = "directory-2026-08-03-university"
  )
)
convenience_result$inserted

It is appropriate when the producing process is itself authorized to accept the records and no person or host policy needs to inspect the change set. Use graft_plan() and graft_commit() separately whenever review matters.

Recover current and historical state

Current retrieval reads the head of the accepted revision chain:

graft_get(store, "person:lois-lane")

Suppose a later source run changes the record:

update <- list(
  person = data.frame(
    id = "person:lois-lane",
    full_name = "Lois Lane",
    job_title = "Investigative editor"
  )
)

update_plan <- graft_plan(
  store,
  update,
  graft_provenance(
    producer = "directory-import",
    run_id = "run-43",
    idempotency_key = "directory-2026-08-04"
  )
)

update_plan@changes
graft_commit(store, update_plan)

graft_history() returns immutable revisions in deterministic newest-first order. Each row carries the accepted record and the provenance needed to explain the change.

history <- graft_history(
  store,
  id = "person:lois-lane",
  limit = 100
)

history[, c("batch_id", "changed_fields", "record")]

An accepted batch ID or POSIXt value can select the state at a commit boundary:

earlier <- graft_history(
  store,
  id = "person:lois-lane",
  as_of = history$batch_id[[2]],
  limit = 1
)

earlier$record[[1]]

Commit order, not a record’s own timestamp, defines historical boundaries.

Verify derived state

Advanced integrity inspection is available as a bounded query:

graft_query(
  store,
  operation = "integrity",
  request = list(projections = TRUE),
  limit = 100
)

The check verifies the authoritative revision chain and, when requested, the freshness of derived projections. A projection can be repaired from accepted revisions; it never becomes a second source of record content.

Evolve the contract explicitly

A domain contract can evolve, but contract evolution is not an in-place rewrite of accepted payloads. A new contract version must be registered, current revisions must be interpretable under it, and derived projections must be rebuilt. Historical revisions retain the exact schema digest under which they were accepted.

During v0.1 development, a breaking contract change means rebuilding a store from source records under the new contract. graft does not hide that change behind a force flag or an automatic transformation.