
Persistent artifacts and selected memory
Source:vignettes/persistent-artifacts.Rmd
persistent-artifacts.RmdSaved work should remain available after an R session ends and after the software that stores it changes. Graft keeps reports, tables, figures, and the exact evidence selected for later workflows. Its decision journal records host reviews and distinguishes historical inspection from current reuse.
Save and reopen exact content
The shared local artifact interface preserves opaque bytes and checks them when they are read. It supports trusted local files, and several R processes can write to one store: a file lock serializes decisions in each stream. The lock needs a file system that honours advisory locks, which some network file systems do not. Applications still need their own rules for approval, access, power-loss durability, and permanent erasure. Saving bytes does not approve them for use.
##
## Attaching package: 'graft'
## The following object is masked from 'package:base':
##
## Recall
path <- tempfile("artifacts-")
store <- graft_store(path, create = TRUE)
original <- graft_save(
store, "Original report", id = "report:daily", media_type = "text/plain"
)
corrected <- graft_save(
store, "Corrected report", id = "report:daily", media_type = "text/plain"
)
reopened <- graft_store(path)
graft_read(reopened, original)@data## [1] "Original report"
graft_read(reopened, corrected)@data## [1] "Corrected report"
unlink(path, recursive = TRUE)ArtifactRef values contain an identity and immutable
revision. Keep them with the workflow that uses them. The store has no
automatic latest or approved selection. This project is pre-production.
Consumer contract 4 uses typed S7 stores and values. The earlier
graph/compiler experiments remain historical evidence, not installed
storage code.
Pin evidence and shared meaning
A dependency is an exact artifact reference. Save dictionary and vocabulary releases as content, then reference them alongside the evidence for a report. The selection preserves their bytes. It does not evaluate expressions or infer that two measurements are comparable.
path <- tempfile("artifact-selection-")
store <- graft_store(path, create = TRUE)
vocabulary <- graft_save(
store, '{"concept":"sample"}',
id = "vocabulary:lab", media_type = "application/json"
)
dictionary <- graft_save(
store, "name: assay", id = "dictionary:assay", media_type = "application/yaml",
dependencies = vocabulary
)
report <- graft_save(
store, "Assay interpretation", id = "report:assay", media_type = "text/plain",
dependencies = dictionary
)
selection <- graft_select(store, report)
reopened_selection <- graft_read_selection(graft_store(path), selection@id)
reopened_selection@artifacts## [[1]]
## <ArtifactRef report:assay@d2a512537a5aeb64afc0579652fe8fb59564e8dec59b3d590110c231b05588d4>
##
## [[2]]
## <ArtifactRef dictionary:assay@514407a409fccc678ce634b5f7794a362960bc555acb66ee3928186e9c853a5f>
##
## [[3]]
## <ArtifactRef vocabulary:lab@084f468d1dd656e8d360372e129b9421e28a561e4f26e48149a354868c14c6b9>
unlink(path, recursive = TRUE)The returned ArtifactSelection includes the report,
dictionary, and vocabulary revisions in @artifacts.
max_metadata_bytes separately limits encoded selection
metadata (1 MiB by default). Increase it when artifact identities are
numerous or long, and pass the same limit when creating and reading the
selection. Likewise, open the store with a larger
max_revision_bytes (default 1 MiB) when dependency lists
create large revision manifests.
max_artifacts limits traversal, and the store’s
max_bytes limits the total selected payload bytes. Reading
checks the full dependency chain again. Save the selection identifier
with the application workflow, then apply the application’s access and
reuse decisions. The decision functions below record application
approval; a selection alone is not accepted memory. The small dictionary
above shows that Graft keeps supplied bytes without interpreting them.
It does not demonstrate a validated data-dict contract.
Record acceptance and withdrawal
Applications decide whether a candidate is acceptable and who may use it. Graft records each decision against an exact selection. A stream names one independently reviewed subject. A new operation key starts a new review even when the selection has not changed.
path <- tempfile("artifact-decisions-")
store <- graft_store(path, create = TRUE)
report <- graft_save(
store, "Reviewed report", id = "report", media_type = "text/plain"
)
accepted <- graft_accept(
store, report, stream = "research:topic", key = "review-1", expected = NULL,
actor = "reviewer:alice",
reason = "Supporting evidence reviewed", purpose = "research"
)
input <- graft_recall(
store, "research:topic", purpose = "research", eligible = TRUE
)
input@artifacts## [[1]]
## <Artifact report@be41974429575fb1dffa3a66cc3e2cc4e3f3d6dd9983ef6cb558e8b3de05d147 media_type=text/plain bytes=15>
withdrawn <- graft_withdraw(
store, "research:topic", expected = accepted, key = "withdraw-1",
actor = "reviewer:alice", reason = "Evidence needs correction"
)
graft_history(store, "research:topic")[[1L]]@action## [1] "accept"
graft_history(store, "research:topic")[[2L]]@action## [1] "withdraw"
unlink(path, recursive = TRUE)The eligible = TRUE argument stands in for the
application’s current check that the material may be used; it does not
authenticate anyone. Recall also requires the current accepted head, an
exact purpose match, and intact selected content. The earlier acceptance
remains available through graft_history() after withdrawal,
but it cannot pass current recall. Authorized historical inspection can
still use exact graft_read() calls. Applications must
enforce access on every route.
Each Decision@selection is an exact selection digest.
Recall@selection is the verified
ArtifactSelection, while Recall@roots and
Recall@artifacts contain materialized Artifact
values. A withdrawal derives its purpose and selection from the verified
predecessor, so callers cannot replace them with changed properties.
List what is kept
graft_streams() returns each stream’s current decision,
so an application can show what it has kept without keeping a separate
catalog. Pass purpose to list only the streams whose
current decision was recorded for that purpose, and check
@action to tell accepted streams from withdrawn ones.
path <- tempfile("artifact-streams-")
store <- graft_store(path, create = TRUE)
report <- graft_save(store, "Reviewed report", id = "report")
graft_accept(
store, report, stream = "project:reactor", key = "keep-1", expected = NULL,
actor = "reviewer:alice", reason = "Kept to the project", purpose = "kept"
)## <Decision accept stream=project:reactor sequence=1 id=7f8e25d1c66d60ead067f55450c776643232cc64a24f23e6e8863d27c36de404>
kept <- graft_streams(store, purpose = "kept")
vapply(kept, \(x) paste(x@stream, x@action), character(1))## [1] "project:reactor accept"
unlink(path, recursive = TRUE)Listing reads decision records only, like
graft_history(). It returns no artifact content and needs
no eligibility decision; graft_recall() still decides
whether the accepted evidence may be used. Stream names, actors, and
reasons are visible in the result, so the application decides who may
list a store. Decisions carry no timestamps, and streams are ordered by
name.
Always supply the predecessor observed during review. An identical retry returns its original historical record and does not restore eligibility. A changed retry or a stale new request fails. Withdrawal needs intact decision history, but it can still proceed when selected payloads are corrupt or unavailable.
Each stream uses a bounded, append-only journal. The defaults allow 1,000 decisions and 1 MiB of aggregate journal metadata; selection bounds are independent. Staging is not a committed decision. A complete immutable record is the commit point, so an interrupted request can be retried without duplicating acceptance. Concurrent writers, power-loss durability, and backup recovery are separate operational work.
From analysis to later reuse
Suppose Commons produces a report, table, and figure. The artifact layer can keep their contents, stable identities, revisions, and references to the exact inputs and meaning used to produce them. A correction creates a new result while the original remains available for historical inspection.
| Concern | Proposed owner |
|---|---|
| Local schemas, types, constraints, and expressions | data-dict |
| Shared concepts and qualified bindings across dictionaries | A separate internal vocabulary publishing module |
| Analysis, context search, and calculation evidence | Commons |
| Artifact contents, revisions, dependencies, and saved selections | Graft shared artifact infrastructure |
| Approval, access, reuse purpose, withdrawal, and active task lifetime | The application |
The vocabulary publisher connects domain concepts across workflows. A binding can associate two local fields with one concept while retaining their source dictionaries and units. It does not establish that their values can be joined or compared.
Persistent artifacts provide material for memory
A later workflow might select an approved interpretation, a dictionary release, and three earlier results. The saved selection records those exact revisions and dependencies. The full selection remains available even when no new acceptance receipt is recorded.
Saving a draft and approving reuse are separate actions. When a new workflow starts, the application checks current eligibility. A correction or a return to an earlier input revision does not reapprove a withdrawn selection. The prototype lets the application approve it again explicitly. The application decides who may do that and for what purpose. Historical access does not grant permission to use the evidence in a new task.
The application also owns cancellation of active work, Reader identity, and permanent Forget. A stored selection cannot erase copies already given to a model.
What replaceable means
Applications retain exact artifacts, dependency selections, and application decisions. They do not depend on graph tables, a schema compiler, or native store objects. A future storage implementation must keep those identities and history. Graft does not yet expose a pluggable backend interface.
Earlier composition experiments compared native graph storage with immutable manifests. Their source and observations were removed from the repository and remain in its git history as research. They led to the artifact contract and the retirement of the native store. Their examples document that earlier architecture and are not instructions for the current store.
Graft keeps the shared infrastructure. Tempest and Rill own product semantics, access, approval, and retention. Publication recovery remains separate work in #49.
Use retained research in Tempest
Tempest maps validated completed research into this artifact
contract. Its tempest_publish_artifact_research() retains
the exact evidence and all of its dependencies, along with source
bodies, program provenance, and a readable report. The application
explicitly accepts that selection with graft_accept().
tempest_reuse_artifact_research() validates the
scientific evidence and checks the current Graft decision through an
application-provided eligibility callback. New runs and resumed sessions
check permission to reuse the evidence again. The callback is transient.
Session bundles retain content and decision provenance, never a saved
permission. Historical inspection uses
tempest_read_artifact_research() and does not grant
reuse.
The Tempest artifact guide contains the installed offline example. Current Tempest consumer tests exercise publication, fresh-process reads, corrections, and current admission through these public interfaces. The earlier pinned experiment suite remains historical evidence. These synthetic fixtures do not establish production access or erasure.
PostgreSQL and application scopes
graft_store_postgres() provides the same artifact,
selection, decision, and vocabulary interfaces inside a DBI transaction
controlled by the application. Its optional DBI and RPostgres
dependencies are needed only for PostgreSQL stores.
DBI::dbWithTransaction(connection, {
# The host derives this scope from authenticated identity and checks access.
store <- graft_store_postgres(
connection, scope = trusted_scope, create = TRUE
)
ref <- graft_save(store, "Accepted text", id = "note", media_type = "text/plain")
accepted <- graft_accept(
store, ref, stream = "note", key = "review-1", expected = NULL,
actor = trusted_actor,
reason = "Explicit review", purpose = "reader-context"
)
# Host catalog and receipt writes can use this same connection.
})
# Only successful transaction completion commits the operation.Every retained row belongs to one scope. Graft serializes operations within a scope until commit or rollback, and the application can retain its own records atomically in the same transaction. A scope key does not authenticate anyone; the database role can still access other scopes directly. Applications must check access before every operation and derive the scope from trusted identity, including in background workers. Connections and store handles are process-local.
This interface does not implement permanent Forget or stop an old database backup from restoring deleted content. The application and storage system must handle those obligations before a broad memory rollout.
Build a verified replacement
An application can preview exclusions with
graft_plan_replacement() and copy the remaining artifacts
into a separate empty store with graft_replace(),
preserving their exact contents. graft_manifest() verifies
the complete logical object set, including historical decisions. See Verify a replacement artifact store
for the runnable example and the separate Forget and restore
obligations.
For a complete copy that retains every object and historical
decision, use graft_backup(). Verification and restore
require the receipt that the application keeps separately from the
bundle. See Back up and restore an
artifact store.