ADR 0061: Research porcelain and one meaning for a ref
ADR 0061: Research porcelain and one meaning for a ref
Section titled “ADR 0061: Research porcelain and one meaning for a ref”Status: Accepted
Implementation: #2126 is the close gate. The porcelain verbs, the ref rule and the two pieces of local state land on one branch; #2127 (agent errors, credentials, skill) and #2128 (Python and Node facades) consume the names decided here and add nothing to them.
Related: #1772 (parent), #1771 (history semantics, done), ADR 0039 (Version publication authority), ADR 0041 (Branch state publishes through Project CURRENT), ADR 0044 (research interchange), ADR 0052 (lineage document), ADR 0053 (Hub publish wire contract), ADR 0055 (Versions are commits).
Context
Section titled “Context”History semantics are finished (#1771): a Version is a commit with parents,
author and committer (ADR 0055); gf clone installs a continuable Branch
(GraphForge::continue_imported_research_branch, crates/graphforge-api/src/research_versions.rs:96);
gf publish fast-forwards only, with --rewrite under an expected-revision
lease and --prune (crates/graphforge-cli/src/hub_publish.rs:81-86); upstream
incorporation records a two-parent Version
(GraphForge::update_research_branch, crates/graphforge-api/src/research_upstream/model.rs:80);
credit is derived and public.
What is missing is the layer an analyst or agent types. Every history operation
is reached through gf research <domain> <verb> --file <native JSON>
(crates/graphforge-cli/src/research_cli/branches.rs:9-13), which requires the
caller to supply the current generation UUID, a fresh operation UUID and a fresh
Version UUID by hand. Versions are addressable only by UUID.
--ref has two meanings today:
- In
gf publish,--ref <name>names a local research Branch by its label;selectfiltersregistry.branchesonrecord.label == nameand refuses zero or several matches (crates/graphforge-cli/src/hub_publish.rs:733-748). The name is checked against the discovery ref grammar first (check_ref_name,hub_publish.rs:781) and is then published as the lineage Branch’sref_nameand as the repository’sRepositoryRef.name. - In
gf clone,--ref <name>is a discovery ref: it is matched againstlineage.branches[*].ref_nameand resolves to that Branch’s head (resolve_research_version_uuid,crates/graphforge-cli/src/hub_clone.rs:402-432).
Because publish writes the label into ref_name unchanged, the two meanings
coincide whenever the label survives the grammar check. The coincidence is an
accident of the publish code path, not a rule: a local Branch may be created with
any label up to 4,096 bytes (crates/graphforge-storage/src/research_versions/branches.rs:153),
including spaces and colons, and only fails at publish time; two Branches may
share a label and only fail at publish time; and a clone records the remote
ref_name (ResearchRemoteBranch.ref_name,
crates/graphforge-storage/src/research_versions/remote.rs:10-22) while the
installed Branch carries whatever label the publisher had.
Nothing names a Version. ResearchVersionRecord.label exists
(crates/graphforge-storage/src/research_versions.rs:186) but is frozen into
the record at creation and is part of the identity digest, so it cannot name a
Version after the fact and cannot be moved. The lineage document carries no
Version names at all (LineageVersion, crates/graphforge-discovery/src/lineage.rs:134).
No local state records which Branch the analyst is working on. Every native
request names its branch_uuid.
Decision
Section titled “Decision”1. A ref is a Branch label, and every Branch label is a valid ref
Section titled “1. A ref is a Branch label, and every Branch label is a valid ref”One rule for gf clone, gf publish, gf push and gf pull:
--ref <name>names a Branch. Locally it is the Branch’slabel; on a Hub it is the lineage Branch’sref_name. They are the same string.
To make the rule true rather than usually true:
- Branch labels are validated as refs at creation.
CreateResearchBranchRequestvalidation (crates/graphforge-api/src/branches/model.rs:36) applies the discovery ref grammar (validate_ref_name,crates/graphforge-discovery/src/lib.rs:1486: non-empty, at most 4,096 bytes, no leading or trailing/, no.., no//, no control bytes, none of\ ? # ~ ^ : space) and refuses a label already held by a Branch of the same Project. The refusal is the existing validation class with codeGF_RESEARCH_BRANCH_LABEL_INVALIDorGF_RESEARCH_BRANCH_LABEL_TAKEN. The storage read-side check is unchanged, so Projects holding older labels still open; such a Branch cannot be pushed (publish already refuses it) andgf statusreports it asunpublishable label. - Publish keeps its grammar check (
check_ref_name) as the defensive boundary and keeps refusing an ambiguous label. After this ADR the ambiguity arm is unreachable for Branches created through the API; it stays as a guard for older Projects. - Clone installs the Branch under the remote ref name.
continue_imported_research_branchalready receives the lineage Branch and itsref_name; the installed Branch record’slabelis the lineagelabel, which the publisher wrote from the same string. The import step assertslabel == ref_nameand refuses a lineage where they differ (hub.malformed_lineage), so a cloned Branch can always be pushed back under the ref it came from. - The default ref is the Hub’s.
gf clone <repository>without--reffollows the repository’s advertiseddefault_ref(#1785, already implemented:hub_listing.rs::clone_without_a_selection_follows_the_default_ref_to_its_branch_head). No porcelain verb invents a default such asmain.
--ref never accepts a Version UUID or a tag. Versions are named by
--version-uuid where a verb accepts one today (gf clone, gf publish), and
by tags through the <version> positional described below.
2. Tags are local, movable until published, and travel in the lineage
Section titled “2. Tags are local, movable until published, and travel in the lineage”A tag is a human name for one Version. It is new persisted state and it is the only new state that travels.
- Local storage.
ResearchRegistrygainstags: BTreeMap<String, Uuid>(crates/graphforge-storage/src/research_versions.rs:256), besideremote_branches. A tag name obeys the ref grammar; a tag target must be in the permanentidentitiesledger (it may name a released Version, like a parent). Bound: at mostMAX_RECEIPTS(4,096) tags, refused as a resource limit. The field is omitted when empty, so revision 7 registry bytes decode and digest unchanged; no format revision. - Mutation. One new
ResearchMutationvariant,SetTag { name, version_uuid: Option<Uuid> }(Nonedeletes), published through the existing registry publication (ADR 0039/0041: atomic CURRENT replacement, receipt, exact replay under the sameoperation_uuid). Setting a tag that already exists with a different target is refused (GF_RESEARCH_TAG_EXISTS); the analyst deletes and re-creates. API:GraphForge::set_research_tag(&SetResearchTagRequest { operation_uuid, expected_generation_uuid, name, version_uuid }). - Travel. The lineage document (ADR 0052) gains
tags: Vec<LineageTag { name, version_uuid }>, canonically ordered by name, omitted when empty so every existing lineage document is byte-identical and still validates. A tag in a lineage must name a Version present inversions. Protocol version is unchanged; a reader of the current crate accepts both shapes, and an older client rejects a document that carries tags underdeny_unknown_fields, which is the pre-v1 policy. - Publish. The lineage a publish uploads carries the union of the served
lineage’s tags and the local tags whose target is in the published closure.
Same name, different target is refused as a non-fast-forward
(
GF_HUB_TAG_CONFLICT) unless--rewrite, under which the local target wins and the rewrite is recorded exactly as a head rewrite is (#2124 lease). Tags are never removed from a Hub by this ADR; a removed local tag stays served. - Clone and pull. The import step that writes
remote_branchesalso writes every lineage tag intotags. On pull, a served tag that conflicts with a local tag of the same name refuses the pull (GF_RESEARCH_TAG_CONFLICT); the analyst deletes the local tag and pulls again. - Resolution. Wherever a porcelain verb takes a
<version>argument it accepts a tag name or a Version UUID, in that order of interpretation; a string that is both a valid UUID and a tag name resolves as a tag (GF_RESEARCH_VERSION_AMBIGUOUSis not needed: UUIDs contain no ref-illegal byte but tags are looked up first, deterministically).gf cloneandgf publish --version-uuidkeep accepting only a UUID;gf clone --tag <name>resolves the name through the lineage’stagsand then follows the existing--version-uuidpath.
The frozen ResearchVersionRecord.label is untouched. It remains the citation
label the native verbs set at creation and is not a tag.
3. The current Branch is local state in the registry
Section titled “3. The current Branch is local state in the registry”ResearchRegistry gains current_branch: Option<Uuid>, omitted when None.
It is written by gf switch through a SelectBranch { branch_uuid } mutation
(GraphForge::select_research_branch) and read by every porcelain verb. It is
local only: it does not appear in the lineage, in interchange archives, or in
any digest, exactly like remote_branches. A clone sets it to the installed
Branch.
Every porcelain verb resolves its Branch in this order and stops at the first
hit: --branch <ref> on the command line; current_branch; the only Branch in
the Project; otherwise GF_RESEARCH_BRANCH_UNSELECTED (“run gf switch <ref>”).
A current_branch that no longer resolves (impossible through the API, which
has no Branch deletion, but possible after restore of an older checkpoint) is
reported by gf status and ignored by the resolver.
4. Top-level verbs, delegating to native operations
Section titled “4. Top-level verbs, delegating to native operations”The porcelain verbs are top level (gf commit, not gf research commit).
Reasons: gf clone, gf publish, gf hub and gf auth are already top
level, and splitting the git workflow across two levels would put clone and
push on different prefixes; gf research stays the namespace of the native
JSON verbs, which keeps “porcelain” and “plumbing” visibly distinct in gf --help.
The one collision, gf research branch (plumbing) versus gf branch
(porcelain), is the git convention (git branch versus git update-ref) and is
acceptable.
Common rules for every verb below:
--jsonis the existing global flag (crates/graphforge-cli/src/lib.rs:247); the JSON result is written by the existingwrite_execution_result/serde_jsonpaths. Human output is text to stdout, one record per line.--branch <ref>is accepted by every verb that acts on a Branch, resolved per section 3.- Verbs that publish a Version take
--operation-uuid <uuid>; when absent the verb generates anow_v7UUID and prints it in both outputs, so a retry with the same request replays the original receipt exactly as the native verb does. The expected generation UUID is read from the open Project immediately before the request (GraphForge::committed_generation_identity), never supplied by the caller; a concurrent writer produces the nativeWriteConflict. - Author and committer come from section 5.
- Hub-facing verbs take
--hub <url>,--token-env <var>and--protection-bypass-env <var>exactly asgf cloneandgf publishdo, and reuse their transport, retry, and credential paths without change.
| Verb | Flags | Native operation | Output |
|---|---|---|---|
gf status |
--branch |
reads only: research_version_retention (branches, heads, ancestry, remote_branches, tags, current_branch) |
branch, head Version, upstream (repository_url ref), ahead N (local Versions not in the ancestry of the recorded remote head), tags on head, warnings (unpublishable label, dangling current_branch). JSON: {branch_uuid, label, head_version_uuid, upstream: {repository_url, ref, head_version_uuid}?, ahead, tags: [..], warnings: [..]} |
gf log |
--branch, --remote [<repository>], -n <count>, --all |
local: walk ancestry from the Branch head through research_version(uuid) records; remote: the gf hub list read of refs and lineage (repository_listing.rs), walking LineageVersion.parents from the ref’s head |
one line per Version: short UUID, tags, author, date, first line of description; --json emits [{version_uuid, parents, author, committer, created_at, label, description, tags, kind}]. --all walks every parent, default is first-parent only |
gf commit |
-m <message> (required), --query <cypher> or --file <path> (exactly one), --branch, --author, --committer, --operation-uuid |
GraphForge::execute_research_branch(&ExecuteResearchBranchRequest{...}), the operation gf research branch execute --file wraps today |
human: [<label> <short uuid>] <subject>; JSON: the native ResearchOperationReceipt plus version_uuid, parents |
gf branch |
none: list; <name>: create from the current head; --from <version>: create from a tag or UUID |
list: research_version_retention; create: GraphForge::create_research_branch with BranchSource::Version { version_uuid: <head or --from> } (branches/model.rs:21), label = <name> |
list: one line per Branch, * on the current one, with head and upstream; create: the native receipt |
gf switch |
<ref> |
select_research_branch (section 3) |
Switched to branch '<ref>' ; JSON {branch_uuid, label} |
gf tag |
none: list; <name> [<version>]: create (default: current head); -d <name>: delete |
set_research_tag (section 2) |
list: <name> <short uuid>; create/delete: the receipt |
gf push |
[<repository>], --ref, --rewrite, --prune, --operation-uuid, --staging-directory, --hub, --token-env, --protection-bypass-env |
hub_publish::run_publish_with with PublishArgs { repository, git_ref: Some(<ref>), version_uuid: None, .. } |
the existing publish result, unchanged |
gf pull |
[<repository>], --ref, --branch, --operation-uuid, --author, --committer, --hub, --token-env, --protection-bypass-env |
(1) download the ref’s head package with the clone transport and verify it with DiscoveryResearchVersionRequest; (2) GraphForge::fetch_research_branch; (3) preview_research_upstream with remote: Some(binding); (4) update_research_branch with ResearchUpstreamSelection::AllCompatible |
human: Merged <remote short uuid> into <ref>: <new short uuid> or Already up to date; JSON: receipt, version_uuid, parents: [local, remote], incorporated, conflicts |
Defaults for gf push and gf pull: <repository> defaults to the
remote_branches record of the resolved Branch (repository_url), and
--ref defaults to that record’s ref_name, which equals the Branch label. A
Branch with no remote record requires <repository> (first push). gf push
never takes --version-uuid, --fork-of or --preview; those remain on
gf publish, which is unchanged and stays the general publication command.
gf pull semantics, stated plainly because they differ from git:
- GraphForge has no working tree. A Branch Version is created by an executed
operation (ADR 0041), so
gf commitalways carries the mutation it commits. There is nothing to stage and nogf add. - Incorporation always records a new Version with parents
[local head, remote head](ADR 0055 table, “Upstream incorporation”). When the local head has not moved since the common base, the content equals the remote head but the Version is still new. The native API offers no pointer-only fast-forward and this ADR adds none;gf pushafter such a pull fast-forwards on the Hub because the new Version descends from the served head. - When the remote head is already in the local ancestry,
gf pullperforms no operation and printsAlready up to date. - When the preview reports changes that
AllCompatiblecannot adopt,gf pullrefuses beforeupdate_research_branch, prints the conflicting fields from the preview, and namesgf research upstream update --fileas the explicit resolution path. The fetch receipt is durable, so the explicit update reuses it.
gf commit -m maps the message to the Version’s existing description field.
ExecuteResearchBranchRequest, ChangeResearchBranchOntologyRequest,
CreateResearchBranchRequest and UpdateResearchBranchRequest gain optional
label and description fields that the Version record already stores
(research_versions.rs:186-188, set to None by these requests today). The
message’s first line is the subject shown by gf log.
5. Author and committer
Section titled “5. Author and committer”Resolution per role, first hit wins:
| Role | Flag | Environment | User configuration |
|---|---|---|---|
| author | --author "Name <email>" |
GF_AUTHOR |
[user] name, email, orcid |
| committer | --committer "Name <email>" |
GF_COMMITTER |
[committer] name, email, orcid; absent: same as author |
The user configuration file is graphforge/config.toml under
$XDG_CONFIG_HOME (default ~/.config) on Unix and %APPDATA% on Windows,
read and written by gf config user [--name <n>] [--email <e>] [--orcid <o>]
(no flags prints the resolved author and committer). It is a new file because
graphforge.yaml is checked-in repository definition
(crates/graphforge-api/src/repository/configuration.rs:130) and identity is
per person, and because gf auth keeps sessions in the OS keyring, which is
the wrong place for public credit. The flag and environment forms parse
Name <email>; ORCID comes only from configuration. Values are validated by the
existing ResearchSignature bounds (ADR 0055) at request time. A verb that
creates a Version with no resolvable author refuses with
GF_RESEARCH_SIGNATURE_MISSING and the two ways to set one. The native JSON
verbs keep taking explicit author/committer in the request and keep
recording none when absent.
6. What stays
Section titled “6. What stays”gf research <domain> <verb> --fileverbs are unchanged in behavior and remain the documented underlying commands. Each help text gets an example or a pointer to the porcelain verb that wraps it (#2126 acceptance).gf cloneandgf publishkeep their names and flags;gf publishgrows no flag;gf clonegrows--tag <name>.- Operation identity, replay, fast-forward,
--rewrite,--prune, the expected-revision lease, and derived credit are the native operations’ and are not reimplemented.
7. Non-goals and what the siblings consume
Section titled “7. Non-goals and what the siblings consume”Non-goals: tag deletion on a Hub; pointer-only fast-forward; Branch deletion or
rename; a staging area; gf merge between local Branches (incorporation is
upstream-only today); Hub refs for tags (tags travel in the lineage, not in
RefSet.refs); gf fetch as a separate verb (fetch is a durable step inside
gf pull; gf log --remote covers inspection without installing anything);
Project-context captures through porcelain (they remain
gf research version prepare and commit).
#2127 consumes: the verb and flag names above for the graphforge-hub skill;
the error codes introduced here (GF_RESEARCH_BRANCH_LABEL_INVALID,
GF_RESEARCH_BRANCH_LABEL_TAKEN, GF_RESEARCH_BRANCH_UNSELECTED,
GF_RESEARCH_TAG_EXISTS, GF_RESEARCH_TAG_CONFLICT, GF_HUB_TAG_CONFLICT,
GF_RESEARCH_SIGNATURE_MISSING), all of class “fix input”, to classify
alongside #1785’s Hub codes; and --token-env as the non-interactive credential
input that gf push and gf pull share with gf clone and gf publish.
#2128 consumes: the native functions each verb maps to (the table in section 4)
so that clone, push, pull and log in Python and Node call the same Rust
operations with the same defaults, and the --json shapes as the typed result
fields.
Alternatives
Section titled “Alternatives”gf research commitand friends under the research namespace. Rejected:clone,publish,hubandauthare already top level, so the workflow would straddle two prefixes, and the namespace is where the plumbing lives.- Map labels to refs with an explicit table (a
ref -> branch_uuidmap instead of validating labels). Rejected: two names per Branch is the ambiguity #1772 complains about, and publish already treats the label as the ref. - Tags as the frozen Version
label. Rejected: the label is part of the identity digest, so it cannot be added after the fact or moved, and nothing publishes it. It stays a citation label. - Tags as Hub refs in
RefSet.refs. Rejected: that changes the Hub wire contract (ADR 0053) and the Hub itself; the lineage document already travels with every publish and clone and is the natural carrier. - Current Branch in a CLI-owned file in the Project directory. Rejected:
the storage crate owns the Project root layout; the registry already holds
local-only durable state (
remote_branches). - A pointer-only fast-forward for
gf pull. Deferred, not rejected: the native API has no head move without a new Version, and adding one is a history-semantics change that belongs to a #1771 follow-up, not to porcelain. gf commitcapturing the Project workspace. Rejected: that is a Project-context capture, which is not a ref and cannot be pushed; Branch Versions are created by executed operations.
Consequences
Section titled “Consequences”- An analyst runs
gf clone,gf commit -m ... --query ...,gf tag,gf push,gf pull,gf logandgf statuswith no JSON and no UUIDs. - A Branch label that is not a valid ref cannot be created; older Projects with
such labels still open and are reported by
gf status. - The registry gains two local fields (
tags,current_branch) and two mutation variants; the lineage gains an optionaltagslist. All are omitted when empty, so existing bytes are unchanged and no revision is bumped. - Four native requests gain optional
labelanddescription. - Every porcelain command performs one native operation (or, for
gf pull, the existing fetch, preview and update sequence), so replay, conflict and credit behavior is the native behavior and is tested there; porcelain tests assert argument mapping and the end-to-end walkthrough only.
Implementation plan
Section titled “Implementation plan”Ordered steps, each a focused commit on feat/2126-research-porcelain:
- Label rule.
crates/graphforge-api/src/branches/model.rs(validate the label withgraphforge_discoveryref grammar and Project uniqueness);crates/graphforge-api/src/research_versions.rs(assertlabel == ref_namewhen installing a cloned Branch). Tests:branches::tests::branch_label_must_be_a_valid_unique_ref,hub_clone::research_clone_tests::clone_refuses_a_lineage_whose_label_differs_from_its_ref. - Registry state.
crates/graphforge-storage/src/research_versions.rs(tags,current_branch,SetTag,SelectBranch, bounds, validation);crates/graphforge-api/src/research_versions.rs(set_research_tag,select_research_branch). Tests:research_versions::tests::tags_are_bounded_and_name_known_identities,research_versions::tests::registry_without_tags_or_current_branch_encodes_as_revision_7,research_versions::tests::set_tag_refuses_a_moved_target_and_replays_exactly. - Lineage tags.
crates/graphforge-discovery/src/lineage.rs(LineageTag, validation, canonical order);crates/graphforge-cli/src/hub_publish/history.rsandpublication.rs(union with served tags,GF_HUB_TAG_CONFLICT,--rewrite);crates/graphforge-api/src/research_versions.rs(install tags at clone). Tests:lineage::tests::tags_are_optional_ordered_and_name_lineage_versions,hub_publish::tests::push_publishes_local_tags_and_refuses_a_moved_served_tag_without_rewrite,hub_clone::research_clone_tests::clone_installs_lineage_tags. - Message fields. Optional
label/descriptionon the four requests;research_versions.rsrecords them. Test:branches::tests::execute_records_label_and_description_on_the_new_version. - Identity configuration. New
crates/graphforge-cli/src/identity.rs(gf config user, resolution order,Name <email>parsing). Tests:identity::tests::author_resolves_flag_then_env_then_file,identity::tests::committer_defaults_to_author,identity::tests::missing_author_refuses_with_signature_missing. - Porcelain module. New
crates/graphforge-cli/src/porcelain.rswithstatus,log,commit,branch,switch,tag,push,pull(pullreuseshub_clonedownload and verification helpers, which move to apub(crate)functiondownload_research_versionwithout behavior change); wire intoCommandinlib.rs. Unit tests per verb assert the constructed native request (porcelain::tests::commit_builds_an_execute_request_on_the_resolved_branch,porcelain::tests::push_defaults_repository_and_ref_from_the_remote_record,porcelain::tests::pull_refuses_before_update_when_the_preview_has_incompatible_changes,porcelain::tests::pull_is_a_no_op_when_the_remote_head_is_an_ancestor,porcelain::tests::branch_resolution_order_is_flag_current_then_only). - Walkthrough.
crates/graphforge-cli/tests/research_porcelain_walkthrough.rs, driving the builtgfbinary (env!("CARGO_BIN_EXE_gf"), asresearch_journey.rs:24does) against the reference Hub started in-process the wayhub_publish/tests.rs::LoopbackHub::startwrapsgraphforge_hub_publish::reference::ReferenceHub::with_config(crates/graphforge-hub-publish/src/reference.rs:82), withGF_AUTHORset in the child environment. Steps and assertions, mapping #2126 acceptance:porcelain_walkthrough_clones_commits_tags_pushes_pulls_and_logs: Alice publishes a Branchmain; Bobgf clone --ref main;gf statusshowsmain, upstream,ahead 0; Bobgf commit -m "first" --query ...andgf commit -m "second" --query ...;gf tag v1;gf push(no arguments); Alicegf pull, thengf commit, thengf push; Bobgf pull; Bobgf loglists both of his commits, Alice’s, and the merge Versions with the expected parents;gf log --remoteagrees with the served lineage;gf log v1through the tag equals the second commit; a thirdgf clone --tag v1materializes the second commit’s content.porcelain_walkthrough_preserves_replay_and_fast_forward: rerunninggf commitwith the printed--operation-uuidreturns the original receipt and no new Version;gf pushfrom a stale clone is refused exactly asgf publishis (GF_HUB_NON_FAST_FORWARDclass), andgf push --rewritesucceeds under the lease.porcelain_walkthrough_uses_no_handwritten_json: the test constructs no request body; asserted by the test containing no--fileinvocation (a grep in the test over its own argv lists).
- Help and docs. Every
gf researchhelp text gains an example or a pointer (command_retirement_tests.rsgainsevery_research_help_text_has_an_example_or_pointer, parsing--helpoutput);crates/graphforge-cli/README.mdgains a “Research workflow” section with every porcelain verb;docs/book/README.mdquick start anddocs/book/research/README.mdare updated with the implemented names;python3 scripts/ci/adr-index.py generateis rerun when this ADR moves to Accepted.
Step 7 is the close evidence for #2126’s walkthrough criterion. Steps 1 to 6 each land with their unit tests; no step introduces a behavior the native API does not already provide except the two state fields and the message fields named above.
Implementation notes
Section titled “Implementation notes”Recorded when #2126 landed; each is a refinement of the text above, not a change of decision.
- Registry revision. The registry is revision 9, not 7.
tagsandcurrent_branchare omitted when empty, so existing bytes and identity digests are unchanged and no revision is bumped (registry_without_tags_or_current_branch_keeps_its_bytes). - Error codes. The
GF_*codes in section 7 are stable prefixes of the validation message, the conventionhub.publish.*uses, not new variants of the closed publicApiErrorCodeset. Tag names are checked against the ref grammar at the API boundary (set_research_tag) and structurally in storage, which does not depend on the discovery crate. Theresearchfeature ofgraphforge-apinow impliesdiscoveryfor the Branch label grammar. --refselects a Branch. Because the label is the ref,gf push --ref Xandgf pull --ref Xselect the Branch labelledX; naming a different--branchis an error.CreateResearchBranchRequestgainsdescriptiononly (its label already is the Branch label); the other three requests gainlabelanddescription.- Operation identity. A caller-supplied
--operation-uuidmust be a UUIDv7. The creation time derives from it. A rerun,gf switchto the current Branch included, is a replay only when its complete request matches the original, otherwise it is refused asGF_IDEMPOTENCY_CONFLICT.commit,branchandpullbind a digest of the whole request (kind, Branch, message, query digest, credit,--from, repository) into the identity of the Version they create, so a receipt whose Version carries a different identity belongs to another request.tagandswitchcarry that digest in the native receipt (intent_sha256), like the other Branch operations, and the native API replays or refuses before it looks at CURRENT. A replay is answered from the local receipt ledger, with no network. A request that names its target relatively (gf tag v1at the head) is the request as resolved; pass the Version to replay it after the head moved. - First Branch.
gf branch <name>in a Project with no Branch creates it from the Project’s current research. - Upstream record. A publication the Hub confirmed (
gf push,gf publish --ref) records the published head as the Branch’s remote citation, the record clone and fetch maintain (ResearchMutation::RecordPublishedBranch, built byrecord_research_publication; the generic operation entry refuses it). It is local tracking state asserted by the caller; the CLI supplies the Hub’s actual responses and nothing authenticates the Hub. The head is not a separate caller field. The call takes the receipt with the manifest and lineage it names (manifest_validatoris the manifest’s digest, which names the lineage’s), requires them to agree with each other and with the Branch, and records the Branch’s head in that lineage, provided it is the Branch head or an ancestor with the identity the lineage names. A replayed publication supplies the served documents when the Project does not track the head it published and they still are the ones the receipt names, and records nothing otherwise, so an interrupted first or later publication is recovered by rerunning it. A citation the Project already holds is left alone, so repeated replays write no receipts. It is local state, not shared history. A publication’s intent bindspublication_content_identity: every participant except the research registry, and the registry without the remote citations and receipts a publication itself writes, so rerunning a completedgf push --operation-uuidreplays it while a tag, Version, canonical decision or Project change does not. - Tags on clone and pull. A lineage tag is installed with the Version it
names. At clone, for Versions the cloned history contains; at
gf pull, by the fetch itself (tagsin the fetch binding), in the same publication as the admission of the fetched Versions, so a pull interrupted before or after the update never leaves served tags half applied. Tags on other Versions stay on the Hub. - Resuming a pull. The fetch’s operation identity derives from the pull’s. A rerun after the fetch committed resumes from it: the fetch binds the CURRENT it was made against, recovered as the parent of the generation the fetch installed, so the recorded binding is reused rather than rebuilt from the newer CURRENT. If that generation has been reclaimed the rerun is refused and a fresh pull (without the identity) starts over.
- Pull conflicts.
conflictanddependency_unavailablepreview rows refuse the pull before any update; the fetch is durable. The evidence acknowledgement is the union required by the adopted rows. - Identity file.
config.tomlis read and edited withtoml_edit:gf config userchanges only the keys it is given and keeps comments, quoting and every unrelated key and table. - Walkthrough. The product transport speaks only HTTPS to public addresses, so
a child
gfcannot reach an in-process Hub. The Hub-facing walkthrough runs the CLI’s own parser and dispatcher in-process over the loopback transport every publish and clone test uses (hub_publish::tests::porcelain), and the built binary drives the verbs that need no Hub (tests/research_porcelain_walkthrough.rs).