Skip to content

feat: attach content metadata to content blobs#69

Merged
flyingrobots merged 4 commits intomainfrom
feature/issue-45-content-metadata
Mar 14, 2026
Merged

feat: attach content metadata to content blobs#69
flyingrobots merged 4 commits intomainfrom
feature/issue-45-content-metadata

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Mar 14, 2026

Summary

Closes #45

Test Plan

  • npx vitest run test/unit/domain/services/PatchBuilderV2.content.test.js test/unit/domain/WarpGraph.content.test.js test/integration/api/content-attachment.test.js
  • npx vitest run test/unit/domain/WarpGraph.apiSurface.test.js -u
  • npm run typecheck
  • npm run typecheck:consumer
  • npm run typecheck:surface
  • npx markdownlint CHANGELOG.md README.md ROADMAP.md docs/specs/CONTENT_ATTACHMENT.md docs/ROADMAP/COMPLETED.md
  • git push origin feature/issue-45-content-metadata (full pre-push firewall)

Summary by CodeRabbit

  • New Features

    • Content attachments now accept optional MIME and size hints; metadata (oid, mime, size) is persisted and retrievable for nodes and edges.
    • New read APIs return structured content metadata.
    • Attachments validate provided size against actual content and fail on mismatch.
  • Documentation

    • Spec and README updated with metadata attach/read examples and legacy-null behavior notes.
  • Tests

    • New integration and unit tests for metadata persistence, retrieval, and validation.

@coderabbitai
Copy link

coderabbitai bot commented Mar 14, 2026

Warning

Rate limit exceeded

@flyingrobots has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 49 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bbbecc1-3a30-4cbb-988c-7386a4d5a482

📥 Commits

Reviewing files that changed from the base of the PR and between bf13805 and f94d44a.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • contracts/type-surface.m8.json
📝 Walkthrough

Walkthrough

Adds optional MIME and byte-size metadata to content attachments, persists metadata as sibling properties, exposes structured reads via getContentMeta/getEdgeContentMeta, and updates Patch/Session/Type declarations, query logic, blob adapter JSDoc, docs, and tests to support metadata-aware flows.

Changes

Cohort / File(s) Summary
Public Types & Declarations
index.d.ts, src/domain/warp/_wiredMethods.d.ts, contracts/type-surface.m8.json
Added ContentAttachmentOptions and ContentMeta types; updated signatures for attachContent/attachEdgeContent to accept metadata and added getContentMeta/getEdgeContentMeta declarations.
Core Key Constants
src/domain/services/KeyCodec.js
Introduced CONTENT_MIME_PROPERTY_KEY (_content.mime) and CONTENT_SIZE_PROPERTY_KEY (_content.size) constants.
Patch Builder & Session
src/domain/services/PatchBuilderV2.js, src/domain/warp/PatchSession.js
attachContent/attachEdgeContent now accept optional metadata, validate/normalize mime+size, include mime/size prop-sets alongside _content, and forward metadata to blob storage calls.
Query & Read Logic
src/domain/warp/query.methods.js, src/domain/warp/_wiredMethods.d.ts
Added register-based helpers (extractContentMeta, getNodeContentRegisters, getEdgeContentRegisters, lineage/visibility helpers) and implemented getContentMeta/getEdgeContentMeta; refactored getContent/getContentOid/getEdgeContent* to derive values from registers/metadata.
Blob Port & Adapter JSDoc
src/ports/BlobStoragePort.js, src/infrastructure/adapters/CasBlobAdapter.js
Expanded JSDoc for Blob store() options to include `mime?: string
Docs & Changelog
CHANGELOG.md, README.md, docs/specs/CONTENT_ATTACHMENT.md
Documented metadata-aware attach APIs, new get*Meta read APIs, legacy semantics for older attachments, and usage examples.
Tests & Type-checks
test/integration/api/content-attachment.test.js, test/unit/domain/WarpGraph.content.test.js, test/unit/domain/services/PatchBuilderV2.content.test.js, test/type-check/consumer.ts
Added/updated tests for node/edge metadata persistence, size validation, blob-store option propagation, and type-check consumer examples.
Project Tracking
ROADMAP.md, docs/ROADMAP/COMPLETED.md
Updated reconciliation date, annotated merged B168 and adjusted backlog counts.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PatchBuilder
  participant BlobStore
  participant WarpGraph
  rect rgba(200,230,255,0.5)
  Client->>PatchBuilder: attachContent(nodeId, content, {mime,size})
  PatchBuilder->>BlobStore: store(content, { slug?, mime, size })
  BlobStore-->>PatchBuilder: oid
  PatchBuilder->>WarpGraph: emit PropSet('_content'=oid)
  PatchBuilder->>WarpGraph: emit PropSet('_content.size'=size)
  PatchBuilder->>WarpGraph: emit PropSet('_content.mime'=mime)
  end
  rect rgba(200,255,200,0.5)
  Client->>WarpGraph: getContentMeta(nodeId)
  WarpGraph->>WarpGraph: read registers (_content, _content.size, _content.mime)
  WarpGraph-->>Client: { oid, mime, size } | null
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇
I hopped to stash each blob with pride,
A tiny mime and size beside.
No more guessing in the night,
OIDs, sizes, MIME — all right! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: attach content metadata to content blobs' is concise, clear, and accurately summarizes the main feature addition in the changeset.
Description check ✅ Passed The description provides a clear summary, references the linked issue (#45), includes a comprehensive test plan, and addresses ADR checks as required by the template.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #45: extends attachContent/attachEdgeContent with optional metadata parameters, exposes getContentMeta/getEdgeContentMeta APIs, stores metadata via sibling properties (_content.mime, _content.size), and includes comprehensive tests [#45].
Out of Scope Changes check ✅ Passed Beyond the core content metadata feature [#45], the PR includes documentation updates to CHANGELOG, README, ROADMAP, and specs which reconcile changes after merged PRs #67/#68. These are necessary documentation maintenance activities directly related to the feature scope.
Docstring Coverage ✅ Passed Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-45-content-metadata
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/infrastructure/adapters/CasBlobAdapter.js (1)

99-121: Note: mime and size options are accepted but not used in CAS storage.

The JSDoc now declares mime and size in the options type, and PatchBuilderV2 passes these values (see src/domain/services/PatchBuilderV2.js lines 607, 637). However, the implementation constructs storeOpts without them — they are silently dropped.

This appears intentional since metadata is persisted separately via CRDT properties (_content.mime, _content.size), but the mismatch between the documented interface and actual behavior may confuse future maintainers. Consider either:

  1. Adding a brief comment explaining why mime/size are accepted but unused, or
  2. Removing them from the JSDoc until CAS-level metadata storage is implemented
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/infrastructure/adapters/CasBlobAdapter.js` around lines 99 - 121, The
JSDoc for async store(content, options) lists mime and size but the
implementation in store() (specifically the storeOpts object) doesn't pass them
to cas.store; add a short inline comment above the storeOpts construction or
update the JSDoc to clarify intent: state that mime and size are accepted for
higher-level CRDT metadata (see PatchBuilderV2) and are intentionally not
forwarded to CAS because CAS metadata is not used here, or alternatively remove
mime/size from the JSDoc; reference the store() method, the storeOpts variable
and PatchBuilderV2 usage when making the change.
test/type-check/consumer.ts (1)

269-273: Strengthen this type-surface test by importing the new public metadata types directly.

Right now the test uses inline object shapes instead of the named API types. Once ContentMeta / ContentAttachmentOptions are exported, using them here will catch export regressions earlier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/type-check/consumer.ts` around lines 269 - 273, Replace the inline
object type shapes in the test with the public exported types: import
ContentMeta and ContentAttachmentOptions (or the exact exported names) at the
top of the test and use them for the variables returned by getContentMeta and
getEdgeContentMeta; update contentMeta and edgeContentMeta declarations to use
ContentMeta | null, and if any option shapes are used elsewhere switch those to
ContentAttachmentOptions to ensure export regressions are caught (reference
symbols: ContentMeta, ContentAttachmentOptions, getContentMeta,
getEdgeContentMeta).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@index.d.ts`:
- Around line 1419-1428: The interfaces ContentAttachmentOptions and ContentMeta
are declared but not exported, yet they're referenced in public signatures
(e.g., PatchBuilderV2 and WarpGraph), so export them from the type definitions;
update the declarations for ContentAttachmentOptions and ContentMeta to be
exported (e.g., export interface ContentAttachmentOptions and export interface
ContentMeta) so consumers can import and use these types in their code and IDEs
can resolve the types referenced by PatchBuilderV2 and WarpGraph.

In `@ROADMAP.md`:
- Around line 398-399: The inventory summary is inconsistent: the "Standalone
(done)" list shows 62 items but the summary text still reads "61 standalone
done"; update the inventory totals so they match by changing the summary text
that currently says "61 standalone done" to "62 standalone done" (look for the
"Standalone (done)" label and the overall inventory summary line that mentions
"61 standalone done" and make them consistent).
- Line 506: The document contains conflicting backlog totals: one header states
"32 active items sorted into priority tiers" while a later sentence says "25
standalone items"; update the text so both locations use the same reconciled
total (choose the correct canonical number for the active backlog), and ensure
any adjacent phrasing (e.g., "standalone items", "active items", "priority
tiers", and "execution waves") is consistent with that chosen number; search for
the exact phrases "32 active items sorted into priority tiers" and "25
standalone items" to locate and update all occurrences including the header and
the Wave summary.

---

Nitpick comments:
In `@src/infrastructure/adapters/CasBlobAdapter.js`:
- Around line 99-121: The JSDoc for async store(content, options) lists mime and
size but the implementation in store() (specifically the storeOpts object)
doesn't pass them to cas.store; add a short inline comment above the storeOpts
construction or update the JSDoc to clarify intent: state that mime and size are
accepted for higher-level CRDT metadata (see PatchBuilderV2) and are
intentionally not forwarded to CAS because CAS metadata is not used here, or
alternatively remove mime/size from the JSDoc; reference the store() method, the
storeOpts variable and PatchBuilderV2 usage when making the change.

In `@test/type-check/consumer.ts`:
- Around line 269-273: Replace the inline object type shapes in the test with
the public exported types: import ContentMeta and ContentAttachmentOptions (or
the exact exported names) at the top of the test and use them for the variables
returned by getContentMeta and getEdgeContentMeta; update contentMeta and
edgeContentMeta declarations to use ContentMeta | null, and if any option shapes
are used elsewhere switch those to ContentAttachmentOptions to ensure export
regressions are caught (reference symbols: ContentMeta,
ContentAttachmentOptions, getContentMeta, getEdgeContentMeta).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f1e82df-d1fc-4c2e-aa51-178c8169d5fb

📥 Commits

Reviewing files that changed from the base of the PR and between 4586045 and f72d03d.

⛔ Files ignored due to path filters (1)
  • test/unit/domain/__snapshots__/WarpGraph.apiSurface.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (17)
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • docs/ROADMAP/COMPLETED.md
  • docs/specs/CONTENT_ATTACHMENT.md
  • index.d.ts
  • src/domain/services/KeyCodec.js
  • src/domain/services/PatchBuilderV2.js
  • src/domain/warp/PatchSession.js
  • src/domain/warp/_wiredMethods.d.ts
  • src/domain/warp/query.methods.js
  • src/infrastructure/adapters/CasBlobAdapter.js
  • src/ports/BlobStoragePort.js
  • test/integration/api/content-attachment.test.js
  • test/type-check/consumer.ts
  • test/unit/domain/WarpGraph.content.test.js
  • test/unit/domain/services/PatchBuilderV2.content.test.js

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
ROADMAP.md (1)

506-506: ⚠️ Potential issue | 🟡 Minor

Backlog total still conflicts with earlier summary text.

Line 506 says 25 active standalone items, but Line 176 still says 32 active items. Please reconcile to a single canonical value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ROADMAP.md` at line 506, The file contains conflicting backlog counts: one
sentence reads "The active backlog is **25 standalone items**" while another
reads "**32** active items"; pick the correct canonical active-items value and
update the other occurrence so both statements match exactly (search for the
phrases "The active backlog is **25 standalone items**" and "**32** active
items**" and replace the incorrect one). Ensure any related summary or
parenthetical counts in the same document are updated too so the total is
consistent across the ROADMAP.md.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/domain/warp/query.methods.js`:
- Around line 368-375: The isSameAttachmentLineage function currently compares
lamport, writerId, and patchSha but omits opIndex, allowing earlier ops in the
same patch to be treated as current; update the function
(isSameAttachmentLineage) to also require contentEventId.opIndex ===
candidateEventId.opIndex (i.e., include an && contentEventId.opIndex ===
candidateEventId.opIndex check alongside the existing lamport/writerId/patchSha
checks) so that operation-order within the same patch is considered when
matching attachment lineage.

---

Duplicate comments:
In `@ROADMAP.md`:
- Line 506: The file contains conflicting backlog counts: one sentence reads
"The active backlog is **25 standalone items**" while another reads "**32**
active items"; pick the correct canonical active-items value and update the
other occurrence so both statements match exactly (search for the phrases "The
active backlog is **25 standalone items**" and "**32** active items**" and
replace the incorrect one). Ensure any related summary or parenthetical counts
in the same document are updated too so the total is consistent across the
ROADMAP.md.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa4b72f3-6e3c-4ffe-a149-fcaf7bbef5f1

📥 Commits

Reviewing files that changed from the base of the PR and between f72d03d and 43bfe8c.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • docs/specs/CONTENT_ATTACHMENT.md
  • src/domain/services/KeyCodec.js
  • src/domain/warp/query.methods.js
  • test/integration/api/content-attachment.test.js
  • test/unit/domain/WarpGraph.content.test.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/specs/CONTENT_ATTACHMENT.md
  • src/domain/services/KeyCodec.js
  • CHANGELOG.md
  • test/unit/domain/WarpGraph.content.test.js

Comment on lines +368 to +375
function isSameAttachmentLineage(contentEventId, candidateEventId) {
return Boolean(
contentEventId
&& candidateEventId
&& contentEventId.lamport === candidateEventId.lamport
&& contentEventId.writerId === candidateEventId.writerId
&& contentEventId.patchSha === candidateEventId.patchSha
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Include opIndex in attachment-lineage matching.

Current lineage matching ignores operation order within the same patch. If _content is rewritten later in that patch, older _content.mime / _content.size from earlier ops in the same patch can be incorrectly treated as current metadata.

Suggested fix
 function isSameAttachmentLineage(contentEventId, candidateEventId) {
   return Boolean(
     contentEventId
       && candidateEventId
       && contentEventId.lamport === candidateEventId.lamport
       && contentEventId.writerId === candidateEventId.writerId
       && contentEventId.patchSha === candidateEventId.patchSha
+      && candidateEventId.opIndex > contentEventId.opIndex
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isSameAttachmentLineage(contentEventId, candidateEventId) {
return Boolean(
contentEventId
&& candidateEventId
&& contentEventId.lamport === candidateEventId.lamport
&& contentEventId.writerId === candidateEventId.writerId
&& contentEventId.patchSha === candidateEventId.patchSha
);
function isSameAttachmentLineage(contentEventId, candidateEventId) {
return Boolean(
contentEventId
&& candidateEventId
&& contentEventId.lamport === candidateEventId.lamport
&& contentEventId.writerId === candidateEventId.writerId
&& contentEventId.patchSha === candidateEventId.patchSha
&& candidateEventId.opIndex > contentEventId.opIndex
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/domain/warp/query.methods.js` around lines 368 - 375, The
isSameAttachmentLineage function currently compares lamport, writerId, and
patchSha but omits opIndex, allowing earlier ops in the same patch to be treated
as current; update the function (isSameAttachmentLineage) to also require
contentEventId.opIndex === candidateEventId.opIndex (i.e., include an &&
contentEventId.opIndex === candidateEventId.opIndex check alongside the existing
lamport/writerId/patchSha checks) so that operation-order within the same patch
is considered when matching attachment lineage.

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/type-surface.m8.json`:
- Around line 1316-1321: The manifest added types ContentAttachmentOptions and
ContentMeta but left API signatures unchanged; update the exported type surface
so PatchBuilderV2.attachContent and PatchBuilderV2.attachEdgeContent, and
PatchSession.attachContent and PatchSession.attachEdgeContent include the
optional metadata parameter (use ContentAttachmentOptions or ContentMeta as
appropriate), and add WarpGraph.getContentMeta and WarpGraph.getEdgeContentMeta
signatures returning ContentMeta; ensure the exports list and function/method
signatures reference those new types to keep the contract consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91f7eeb8-1980-4983-a886-3c0089ba98c7

📥 Commits

Reviewing files that changed from the base of the PR and between 43bfe8c and bf13805.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • ROADMAP.md
  • contracts/type-surface.m8.json
  • index.d.ts
  • src/infrastructure/adapters/CasBlobAdapter.js
  • test/type-check/consumer.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • index.d.ts
  • test/type-check/consumer.ts
  • CHANGELOG.md
  • src/infrastructure/adapters/CasBlobAdapter.js

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

@flyingrobots flyingrobots merged commit e6e8e86 into main Mar 14, 2026
7 checks passed
@flyingrobots flyingrobots deleted the feature/issue-45-content-metadata branch March 14, 2026 03:10
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.

feat: attach MIME type and byte size metadata to content blobs

1 participant