Skip to content

ArtifactRef: one spelling for artifact:[<store>/]<key>[@<sha256>], plus ArtifactStore.resolve - #95

Merged
thorwhalen merged 3 commits into
mainfrom
artifact-ref
Oct 27, 2026
Merged

thorwhalen merged 3 commits into
mainfrom
artifact-ref

Conversation

@thorwhalen

Copy link
Copy Markdown
Collaborator

Closes #90

One spelling for a reference to an ArtifactStore artifact, artifact:[<store>/]<key>[@<sha256>], and one way to read what it names.

API (lacing.artifact_ref, lacing.artifact_store; exported from lacing):

  • ArtifactRef(key, store=None, pin=None), a frozen dataclass:
    • .parse(text, *, require_prefix=True): require_prefix=False also reads an's unprefixed manifest form <store>/<key>@<sha256>.
    • .format().
    • .locate(mall, *, default_store): decides the store segment against a mall (an's rule). The text alone cannot tell takes/a (key a of store takes) from a key takes/a, so parse keeps the whole name as key. Any reference that format writes reads back as itself; the constructor refuses one that would not.
  • ArtifactStore.resolve(ref, *, read=True) -> ResolvedArtifact(key, record, content_hash, data):
    • A pinned read returns the pinned bytes even after the key was re-saved or deleted, and checks them against the pin (SHA-256, as save computes it).
    • The key is checked first; the catalog is scanned only when the key no longer holds the pin.
    • read=False checks that the bytes exist without reading them, for streaming callers (iter_blob, blob_path).
  • resolve_in_mall(ref, mall, *, default_store, read=True) does locate-then-resolve. "No such store" raises a plain LookupError, distinct from KeyError.
  • content_hash_of(record): asset_id, else content_hash (reelee's records). This is now the one rule used by count_refs, from_sql and resolve.
  • ARTIFACT_REF_PREFIX. is_artifact_ref stays in lacing.artifact_ref and is not exported from lacing, because an's predicate also accepts Artifact objects.

Acceptance. an and reelee parse and format through the helper, and a pinned reference round-trips (tests). Adoption in an and reelee is filed in those repos; other workers hold them. The differences from an's parse_artifact_ref are listed in the module docstring:

  • takes/ and takes//a go to the default store;
  • text without the prefix raises unless require_prefix=False;
  • artifact: x with a space is refused.

reelee ids with inner whitespace cannot be formatted as references.

Review (per the burn's rule):

  1. Adversarial review 1 (fresh Opus): FIX FIRST.
    • resolve refused every reelee record (content_hash, not asset_id).
    • Every pinned read listed the whole catalog.
    • BLAKE3 pins vs the SHA-256 check.
    • ref.store was silently ignored, with no mall-level path.
    • ResolvedArtifact lacked key and hash and forced full reads.
    • is_artifact_ref and parse could disagree.
    • Tests were missing.
  2. Refactor (Opus subagent): content_hash_of shared with count_refs and from_sql, the key-first lazy holder scan, the new ResolvedArtifact and read=False, resolve_in_mall, the SHA-256 pin doc, and 66 tests.
  3. Re-review (fresh Opus): FIX FIRST. All fixed here:
    • an's stored manifest references have no prefix: require_prefix=False.
    • locate called bool() on the default store, which lists a catalog: now is None.
    • The public keys_with_content broke a delete-while-iterating loop and hid damaged records: now private.
    • An uppercase pin silently became part of the key: now accepted and lowercased.
    • "No such store" and "no such key" were both KeyError: now LookupError vs KeyError.
    • A non-str raised AttributeError: now False / TypeError.
    • __str__ = format hid the parts: dropped.
    • is_artifact_ref is no longer exported from lacing.

CI: Windows tests are off for this repo (test_on_windows = false), so that job skips, as on every PR here.

Follow-ups filed after merge:

  • an adoption;
  • reelee adoption;
  • an indexed content-hash → keys lookup for SQL catalogs (the holder fallback is a full scan today).

🤖 Generated with Claude Code

thorwhalen and others added 3 commits October 27, 2026 18:05
…us ArtifactStore.resolve

an and reelee each spelled a reference to an ArtifactStore artifact
their own way. ArtifactRef.parse/format is the one parser and formatter;
locate() decides the store segment against a mall (an's rule, since the
text alone cannot tell store/key from a key with a slash), and
ArtifactStore.resolve(ref) returns the record and bytes, reading a pinned
blob even after its key was re-saved and checking it hashes to the pin.

Closes #90

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h in ResolvedArtifact, resolve_in_mall

- content_hash_of(record): asset_id, else content_hash, else None; used by
  count_refs, from_sql's default, resolve and the holder scan. A record with
  no content yet raises KeyError.
- ArtifactStore.keys_with_content: a lazy catalog scan that skips damaged
  records. resolve checks ref.key first and scans only when the key no longer
  holds the pin.
- ResolvedArtifact(key, record, content_hash, data); resolve(ref, *, read=True)
  with read=False checking existence only (no pin hash check). blobs=None
  raises KeyError.
- resolve_in_mall(ref, mall, *, default_store, read=True).
- is_artifact_ref is exactly "parse succeeds"; parse raises one ValueError,
  chained to the key's error; locate splits only when the rest is a key.
- Exports ARTIFACT_REF_PREFIX, content_hash_of, resolve_in_mall; module note on
  differences from an's parse_artifact_ref.

Refs #90

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…locate, private scan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One artifact reference syntax: artifact:[<store>/]<key>[@<sha256>] (an and reelee)

1 participant