Skip to content

Commit d3a46c2

Browse files
committed
feat: make ref names application policy, not store-imposed layout
Reworks gix-store's publish/kind machinery so callers choose alias paths independently of the CAS parent oid, instead of the store deriving a fixed ref shape. Updates the migration transcript for the nested comment and literal rules/review ref layouts this uncovered.
1 parent b824a18 commit d3a46c2

12 files changed

Lines changed: 485 additions & 841 deletions

File tree

‎README.adoc‎

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,25 @@ Those names describe metadata found on historical Git objects only. Readers
2222
ignore legacy trailers, including malformed or conflicting values, and never
2323
use them to choose a schema.
2424

25-
The old named entity CLI/API forms and named refs are historical compatibility
26-
paths. They may remain useful for addressing older entities, but they are not
27-
the document's type definition or content identity.
25+
Names remain how entities are addressed. A name is not the document's type
26+
definition and not its content identity, both of which live in the document
27+
itself.
2828

2929
== Entity Identity and Refs
3030

3131
`EntityId` is the OID of the complete bound document tree: the root containing
3232
exactly `schema/` and `value/`. It is not the OID of the publication commit and
33-
it is not a caller-selected alias. Both the embedded schema and encoded value
34-
therefore contribute to the identity. The canonical data ref is
35-
`refs/store/<kind>/<entity-id>`; named paths beneath the same kind are
36-
compatibility aliases and are excluded from the canonical entity index.
37-
38-
The current `Kind` API exposes both layers. `put_entity`, `compile_entity`,
39-
`read_entity`, and `entity_reference` use the content-derived identity.
40-
`put`, `get`, `reference`, `entity_name`, `anonymous`, and related named forms
41-
are compatibility conveniences for ref-addressed callers. A named write may
42-
maintain an alias, but the name never becomes the entity's identity.
33+
it is not a caller-selected name. Both the embedded schema and encoded value
34+
therefore contribute to the identity.
35+
36+
Identity is a derived value, not a ref layout. Data refs are
37+
`refs/store/<kind>/<name>`, and the name is whatever the caller chooses: the
38+
store attaches no meaning to it and advances it like a branch. Applications
39+
decide what names mean. An application that wants content-addressed storage
40+
selects `<entity-id>` as the name, which the store treats like any other;
41+
`put_entity`, `read_entity`, and `entity_reference` are the shorthand for that
42+
policy, and `compile_entity` derives the id for any document without publishing
43+
it.
4344

4445
== Deletion and CLI Compatibility
4546

@@ -55,16 +56,15 @@ The primary CLI form, `git store get <tree-ish>`, decodes a bound document from
5556
the tree it names. The hidden compatibility form, `git store get <kind> <name>`,
5657
reads a named ref and reports deleted entities differently from missing ones;
5758
revision suffixes such as `name~1` and `name@<revision>` address history.
58-
`git store rm <kind> <name>` publishes a tombstone over the canonical ref and
59-
any alias pointing at that publication. It does not hard-delete refs; repeating
59+
`git store rm <kind> <name>` publishes a tombstone over that name. It does not
60+
hard-delete refs, so the name's earlier publications stay reachable; repeating
6061
it reports `already deleted`, while an absent ref is an error. `get` reports a
61-
tombstone as deleted and an absent ref as absent.
62+
tombstone as deleted and an absent ref as absent. Names are independent:
63+
deleting one name does not affect another that addresses the same content.
6264

63-
Writing the exact same bound content after deletion restores its original
64-
canonical ID by appending a normal value commit after the tombstone; aliases
65-
that pointed at the tombstone are restored atomically. Writing different
66-
content creates a new ID, leaving the old ID deleted. The CLI has no separate
67-
restore command; recreation and restoration are library write operations.
65+
Writing to a deleted name restores it by appending a normal value commit after
66+
the tombstone. The CLI has no separate restore command; recreation and
67+
restoration are ordinary write operations.
6868

6969
== Trailer and Schema Compatibility
7070

@@ -142,23 +142,23 @@ emit a success record. The current exit categories are:
142142
=== Publication, CAS, and Identity
143143

144144
`document publish <kind> <document-tree> --expected <absent|OID>` publishes a
145-
prepared bound document. With no `--alias`, the expectation applies to the
146-
canonical ref; with `--alias <name>`, it applies to that compatibility alias.
147-
The canonical ref, optional alias, and materialized per-kind index advance in
148-
one compare-and-swap batch. A stale expectation is a one-shot failure: the CLI
145+
prepared bound document under `--alias <name>`, defaulting to the entity's
146+
content-derived name when none is given. The expectation applies to that ref,
147+
and the ref and the materialized per-kind index advance in one compare-and-swap
148+
batch. A stale expectation is a one-shot failure: the CLI
149149
returns exit category `4` and does not retry. Objects written before a lost CAS
150150
may remain unreachable. A script owns retry, resume, and conflict policy.
151151

152152
The complete bound document-tree OID is the content-derived `EntityId`, and both
153-
its schema and value contribute to it. The canonical ref is
154-
`refs/store/<kind>/<entity-id>`. Names are aliases, not identity. The persisted
155-
per-kind index under `refs/cache/` is a materialized cache of canonical refs;
156-
entity refs remain authoritative and reads can fall back when the index is
157-
missing, malformed, or stale.
158-
159-
`entity delete <kind> <entity-id>` publishes a typed tombstone over the
160-
canonical ref and aliases/index atomically. It does not remove the ref. A
161-
repeated deletion reports `already_deleted`; an absent entity is distinct.
153+
its schema and value contribute to it. It is a derived value, available for any
154+
document that compiles, and is independent of where the document is published.
155+
The persisted per-kind index under `refs/cache/` is a materialized cache of the
156+
entity refs; those refs remain authoritative and reads can fall back when the
157+
index is missing, malformed, or stale.
158+
159+
`entity delete <kind> <name>` publishes a typed tombstone over that ref,
160+
advancing it and the index atomically. It does not remove the ref. A repeated
161+
deletion reports `already_deleted`; an absent entity is distinct.
162162

163163
=== Script Boundary
164164

‎crates/git-store/README.md‎

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ $ git store get <document-tree> | jq .serves
3030

3131
The positional `put <schema> [<value>]` form remains a convenient pure compile:
3232
it prints the bound document-tree OID and advances no ref. The hidden two-argument
33-
`put <kind> <name>` form is retained for old scripts and publishes a named
34-
compatibility alias. The explicit `value` and `document` commands make each
33+
`put <kind> <name>` form publishes the value under that name. The explicit
34+
`value` and `document` commands make each
3535
step addressable for a manually scripted migration.
3636

3737
What landed is a real Git tree, not an opaque payload — `git ls-tree` and
@@ -54,7 +54,7 @@ $ git ls-tree refs/store/recipe/carbonara^{tree}:value
5454
$ git cat-file blob $(git rev-parse refs/store/recipe/carbonara^{tree}:value/serves)
5555
4
5656

57-
$ git log --oneline refs/store/recipe/carbonara # compatibility-alias history
57+
$ git log --oneline refs/store/recipe/carbonara # this name's history
5858
9f50ef7 publish recipe
5959
15e425b publish recipe
6060

@@ -119,9 +119,9 @@ git store # list kinds
119119
git store put <schema> [<value>] # pure compile; prints document-tree OID
120120
git store get <tree-ish> # decode a bound document
121121
git store check <tree-ish> <schema> # validate a value tree against a published schema
122-
git store list [<kind>] (alias: ls) # kinds, or live entity aliases
123-
git store log <kind> <name> # commit OID + date per compatibility alias
124-
git store rm <kind> <name> # compatibility-name tombstone
122+
git store list [<kind>] (alias: ls) # kinds, or live entity names
123+
git store log <kind> <name> # commit OID + date per publication
124+
git store rm <kind> <name> # publish a tombstone over a name
125125
git store schema put <kind> [-F <file>] # define/evolve a kind (else stdin, or -i)
126126
git store schema get <kind> [--at <commit>] [--legacy-leaves] # schema as JSON
127127
git store schema inspect <kind> --at <commit> [--legacy-leaves] # inspect publication
@@ -137,7 +137,7 @@ git store value decode <value-tree> --schema <tree-ish>
137137
git store document inspect <document-tree>
138138
git store document bind <value-tree> --schema <schema-tree>
139139
git store document publish <kind> <document-tree> --expected <absent|OID> [--alias <name>]
140-
git store entity delete <kind> <entity-id> # canonical tombstone
140+
git store entity delete <kind> <name> # typed tombstone over a name
141141
142142
# Global layout options (defaults shown):
143143
git store --data-prefix refs/store --schema-prefix refs/schema <command>
@@ -200,8 +200,8 @@ tree; no kind lookup, schema-history guess, or trailer is consulted.
200200
`document bind <value-tree> --schema <schema-tree>` creates only the complete
201201
`{schema/, value/}` root tree. `document inspect <document-tree>` reports
202202
`bound`, `legacy_value_root`, or `malformed` without guessing. `document publish`
203-
then commits a prepared bound tree and updates the canonical ref, optional alias,
204-
and materialized index in one compare-and-swap batch.
203+
then commits a prepared bound tree and updates the named ref and materialized
204+
index in one compare-and-swap batch.
205205

206206
`schema get <kind> --at <commit>` returns the schema snapshot at an explicit
207207
publication commit; without `--at` it returns the current snapshot. `schema
@@ -235,21 +235,23 @@ for the actionable diagnostic.
235235
### Publication, identity, and deletion
236236

237237
`document publish` requires `--expected absent` or `--expected <full-oid>`. With
238-
no alias the expectation applies to the selected data prefix's
239-
`<data-prefix>/<kind>/<entity-id>`; with `--alias` it applies to that alias. A
240-
stale expectation fails with exit category
238+
`--alias <name>` it publishes at `<data-prefix>/<kind>/<name>`; with no alias it
239+
publishes at the entity's content-derived name. The expectation applies to that
240+
ref. A stale expectation fails with exit category
241241
`4`, publishes none of the batch, and is **not retried**. Objects written before
242242
a lost CAS may remain unreachable; a script decides whether to retry, resume, or
243243
report the conflict. The CLI does not hide that policy.
244244

245245
The bound document-tree OID is the content-derived `EntityId`, so both schema
246-
and value contribute to identity. The canonical ref is
247-
`<data-prefix>/<kind>/<entity-id>`. Caller names are compatibility aliases and
248-
are not identity or the source of truth. The per-kind index under `refs/cache/` is
249-
a materialized cache of canonical refs: entity refs remain authoritative, and
250-
readers can fall back to them when the index is absent, malformed, or stale.
251-
`entity delete <kind> <entity-id>` publishes a typed tombstone over the
252-
canonical ref and updates aliases/index atomically; it does not prune the ref.
246+
and value contribute to identity. It is a derived value, independent of where
247+
the document is published. Ref layout is application policy: an entity lives at
248+
`<data-prefix>/<kind>/<name>` for whatever name the caller picks, and naming it
249+
`<entity-id>` is one such choice rather than a rule the store imposes. The
250+
per-kind index under `refs/cache/` is a materialized cache of the entity refs:
251+
those refs remain authoritative, and readers can fall back to them when the
252+
index is absent, malformed, or stale. `entity delete <kind> <name>` publishes a
253+
typed tombstone over that ref and updates the index atomically; it does not
254+
prune the ref.
253255

254256
### Script boundary
255257

‎crates/git-store/src/main.rs‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use clap::{Parser, Subcommand, ValueEnum};
2929
use facet_git_tree::{Node, Schema, SchemaSchema, VariantKind, validate_with_schema};
3030
use facet_value::{VArray, VObject, Value, from_value};
3131
use gix_store::{
32-
ApplyError, DeleteResult, DocumentInspection, Dynamic, EntityId, EntityState, Expectation,
32+
ApplyError, DeleteResult, DocumentInspection, Dynamic, EntityState, Expectation,
3333
GixRefStore, Kind, Layout, ObjectId, PublishOptions, RefName, RefPath, RefPrefix, RefSegment,
3434
RefStore, RepoStore,
3535
};
@@ -1230,13 +1230,10 @@ fn entity_delete(
12301230
entity_text: &str,
12311231
format: OutputFormat,
12321232
) -> Result<()> {
1233-
let id: EntityId = entity_text.parse().with_context(|| {
1234-
cli_context(
1235-
ExitClass::Invalid,
1236-
format!("invalid canonical entity id {entity_text:?}"),
1237-
)
1238-
})?;
1239-
let result = store.dynamic(segment("kind", kind)?).delete_entity(id)?;
1233+
// Names are application policy: an entity may be published under a
1234+
// content-derived id or any other path, and either addresses it here.
1235+
let name = entity(entity_text)?;
1236+
let result = store.dynamic(segment("kind", kind)?).delete_name(&name)?;
12401237
let (status, code, commit) = match result {
12411238
DeleteResult::Deleted(entry) => ("deleted", "deleted", Some(entry.commit)),
12421239
DeleteResult::AlreadyDeleted(entry) => {
@@ -1245,7 +1242,7 @@ fn entity_delete(
12451242
DeleteResult::Absent => {
12461243
return Err(cli_error(
12471244
ExitClass::NotFound,
1248-
format!("no entity {kind}/{id}"),
1245+
format!("no entity {kind}/{entity_text}"),
12491246
));
12501247
}
12511248
};
@@ -1254,13 +1251,13 @@ fn entity_delete(
12541251
record.insert("status", status);
12551252
record.insert("code", code);
12561253
record.insert("kind", kind);
1257-
record.insert("id", id.to_string());
1254+
record.insert("id", entity_text.to_owned());
12581255
if let Some(commit) = commit {
12591256
record.insert("commit", oid_value(commit));
12601257
}
12611258
emit_record(format, record)
12621259
} else {
1263-
println!("{status} {kind}/{id}");
1260+
println!("{status} {kind}/{entity_text}");
12641261
Ok(())
12651262
}
12661263
}

‎crates/git-store/tests/cli.rs‎

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,7 @@ fn store_get_list_and_remove() {
198198
let (out, _, ok) = run(path, None, &["ls", "recipe"]);
199199
assert!(ok);
200200
let listed: Vec<_> = out.lines().collect();
201-
assert_eq!(listed.len(), 2);
202-
assert!(listed.contains(&"carbonara"));
203-
let older_canonical = listed
204-
.iter()
205-
.copied()
206-
.find(|name| name.len() == 40)
207-
.expect("older canonical identity");
201+
assert_eq!(listed, ["carbonara"]);
208202

209203
let (out, _, ok) = run(path, None, &["schema", "show", "recipe"]);
210204
assert!(ok);
@@ -244,7 +238,7 @@ fn store_get_list_and_remove() {
244238
let (out, err, ok) = run(path, None, &["ls", "recipe"]);
245239
assert!(ok, "list after rm failed: {err}");
246240
let listed_after_delete: Vec<_> = out.lines().collect();
247-
assert_eq!(listed_after_delete, [older_canonical]);
241+
assert!(listed_after_delete.is_empty(), "list after rm: {out}");
248242
assert!(
249243
!listed_after_delete.contains(&"carbonara"),
250244
"deleted alias must not list as live: {out}"
@@ -617,8 +611,7 @@ fn entity_names_may_nest() {
617611
.unwrap();
618612
assert_eq!(
619613
String::from_utf8_lossy(&refs.stdout).trim(),
620-
"refs/store/recipe/2551a112682de0d7564849a0ab4d9d9a1bf6aaf1\
621-
\nrefs/store/recipe/italian/carbonara"
614+
"refs/store/recipe/italian/carbonara"
622615
);
623616
}
624617

@@ -1277,15 +1270,15 @@ fn composable_plumbing_supports_json_ndjson_and_explicit_cas() {
12771270
path,
12781271
None,
12791272
&[
1280-
"entity", "delete", "recipe", &entity_id, "--format", "ndjson",
1273+
"entity", "delete", "recipe", "carbonara", "--format", "ndjson",
12811274
],
12821275
);
12831276
assert!(ok, "entity delete failed: {err}");
12841277
assert_eq!(json_string_field(&deleted, "status"), "deleted");
12851278
let (already, err, ok) = run(
12861279
path,
12871280
None,
1288-
&["entity", "delete", "recipe", &entity_id, "--json"],
1281+
&["entity", "delete", "recipe", "carbonara", "--json"],
12891282
);
12901283
assert!(ok, "repeated entity delete failed: {err}");
12911284
assert_eq!(json_string_field(&already, "status"), "already_deleted");

0 commit comments

Comments
 (0)