Skip to content

Add file-ref field kind so content-held file references are real references#91

Merged
cuibonobo merged 2 commits into
mainfrom
claude/issue-63-design-review-l62w48
Jul 13, 2026
Merged

Add file-ref field kind so content-held file references are real references#91
cuibonobo merged 2 commits into
mainfrom
claude/issue-63-design-review-l62w48

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

Closes #63.

The spec's attachment-delete contract promises a 409 Conflict when a record's content references a fileId, not just when an attachment Association does — but there was no way to distinguish "content that references a file" from "content that happens to contain 64 hex characters." This adds file-ref as a scalar field kind to close that gap:

  • FieldDef gains kind: 'file-ref' (packages/core/src/types.ts). Values must be well-formed fileIds (SHA-256 hex) — validated at write time, referential existence is not (same stance as record-ref).
  • isCompatible() treats file-ref as exact-match only.
  • Top-level file-ref content fields are now indexed the same way attachment Associations are: the attachmentFileId query filter, deleteAttachment()'s reference check (including the atomic deleteUnreferencedAttachmentRecords path), and ScopedStack attachment-access conveyance all now see a fileId held in a file-ref field.
  • SQLite-backed adapters (sqlite-shared) track this via a new file_refs index table, kept in sync on every write that can change content or typeId (createRecord, patchContent, restoreVersion, commitMigration), and cleaned up on hard delete.
  • MemoryAdapter computes the same match at query time against the record's registered type schema (no persisted index needed for the in-memory test double).
  • A fileId stored in a plain string field continues to work but gets none of this — no delete protection, no access conveyance, no future GC protection. Documented explicitly in docs/spec.md.

Scoped to top-level scalar file-ref fields only, matching the existing limitation that only top-level scalars support exact-match content filtering in queries.

Test plan

  • packages/core/tests/validate.test.ts — file-ref format validation (accepts well-formed SHA-256 hex, rejects wrong length/case/non-hex/non-string)
  • packages/core/tests/schema.test.ts — file-ref is exact-match-only in isCompatible()
  • packages/core/tests/stack.test.tsdeleteAttachment() blocked by a content-held file-ref field; unaffected by a plain string field with the same value
  • packages/core/tests/scoped-stack.test.ts — attachment access conveyed by a readable record's file-ref field; not conveyed by an equivalent string field
  • packages/record-adapter-sqlite/tests/record.test.ts and packages/record-adapter-sqljs/tests/record.test.tsfile_refs index population/query matching, updates on patchContent, cleanup on hard delete, and the atomic delete-block path
  • Full monorepo build, typecheck, lint, and test suite (611 tests) pass

🤖 Generated with Claude Code


Generated by Claude Code

claude added 2 commits July 13, 2026 17:31
…re real references (#63)

Adds `file-ref` as a scalar FieldDef kind: values must be well-formed
SHA-256 hex fileIds, and top-level file-ref content fields are now
indexed the same way attachment associations are. `attachmentFileId`
query matching, deleteAttachment()'s reference check, and attachment
access conveyance under ScopedStack all now see a fileId held in a
file-ref field — closing the gap where a fileId in plain content was
invisible to the reference checks the spec already promised. A fileId
in a plain `string` field still conveys none of this, by design.

SQLite-backed adapters track this via a new file_refs index table kept
in sync on every content/typeId write; MemoryAdapter checks the
record's registered type schema at query time instead.
… re-querying on every write

syncFileRefs() was hitting the types table and JSON.parse-ing the full
schema on every single createRecord/patchContent/restoreVersion/
commitMigration call, even though most types have no file-ref fields
at all. Cache the file-ref field names per typeId (populated in
saveType(), lazily filled for types saved before this adapter instance
existed) so that lookup happens once per type instead of once per write.
@cuibonobo cuibonobo merged commit 35a8d4e into main Jul 13, 2026
5 checks passed
@cuibonobo cuibonobo deleted the claude/issue-63-design-review-l62w48 branch July 13, 2026 19:42
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.

file-ref field kind: content-held file references are invisible to the reference checks the spec says exist

2 participants